Merge pull request #30659 from hashicorp/alisdair/condition-blocks-sensitive-values
core: Fix crashes when condition block expressions refer to sensitive values
This commit is contained in:
commit
24d174d36e
|
@ -2859,3 +2859,65 @@ func TestContext2Plan_preconditionErrors(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Plan_preconditionSensitiveValues(t *testing.T) {
|
||||||
|
p := testProvider("test")
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "boop" {
|
||||||
|
sensitive = true
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
output "a" {
|
||||||
|
sensitive = true
|
||||||
|
value = var.boop
|
||||||
|
|
||||||
|
precondition {
|
||||||
|
condition = length(var.boop) <= 4
|
||||||
|
error_message = "Boop is too long, ${length(var.boop)} > 4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
||||||
|
Mode: plans.NormalMode,
|
||||||
|
SetVariables: InputValues{
|
||||||
|
"boop": &InputValue{
|
||||||
|
Value: cty.StringVal("bleep"),
|
||||||
|
SourceType: ValueFromCLIArg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatal("succeeded; want errors")
|
||||||
|
}
|
||||||
|
if got, want := len(diags), 2; got != want {
|
||||||
|
t.Errorf("wrong number of diags, got %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
for _, diag := range diags {
|
||||||
|
desc := diag.Description()
|
||||||
|
if desc.Summary == "Module output value precondition failed" {
|
||||||
|
if got, want := desc.Detail, "The error message included a sensitive value, so it will not be displayed."; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected detail\ngot: %s\nwant to contain %q", got, want)
|
||||||
|
}
|
||||||
|
} else if desc.Summary == "Error message refers to sensitive values" {
|
||||||
|
if got, want := desc.Detail, "The error expression used to explain this condition refers to sensitive values."; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected detail\ngot: %s\nwant to contain %q", got, want)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("unexpected summary\ngot: %s", desc.Summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/hashicorp/terraform/internal/configs"
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
"github.com/hashicorp/terraform/internal/instances"
|
"github.com/hashicorp/terraform/internal/instances"
|
||||||
"github.com/hashicorp/terraform/internal/lang"
|
"github.com/hashicorp/terraform/internal/lang"
|
||||||
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -110,6 +111,10 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The condition result may be marked if the expression refers to a
|
||||||
|
// sensitive value.
|
||||||
|
result, _ = result.Unmark()
|
||||||
|
|
||||||
if result.True() {
|
if result.True() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -127,10 +132,26 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
Expression: rule.ErrorMessage,
|
Expression: rule.ErrorMessage,
|
||||||
EvalContext: hclCtx,
|
EvalContext: hclCtx,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
if marks.Has(errorValue, marks.Sensitive) {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: severity,
|
||||||
|
|
||||||
|
Summary: "Error message refers to sensitive values",
|
||||||
|
Detail: `The error expression used to explain this condition refers to sensitive values. Terraform will not display the resulting message.
|
||||||
|
|
||||||
|
You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`,
|
||||||
|
|
||||||
|
Subject: rule.ErrorMessage.Range().Ptr(),
|
||||||
|
Expression: rule.ErrorMessage,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
|
errorMessage = "The error message included a sensitive value, so it will not be displayed."
|
||||||
} else {
|
} else {
|
||||||
errorMessage = strings.TrimSpace(errorValue.AsString())
|
errorMessage = strings.TrimSpace(errorValue.AsString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if errorMessage == "" {
|
if errorMessage == "" {
|
||||||
errorMessage = "Failed to evaluate condition error message."
|
errorMessage = "Failed to evaluate condition error message."
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/hashicorp/hcl/v2/gohcl"
|
"github.com/hashicorp/hcl/v2/gohcl"
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/configs"
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
"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"
|
||||||
|
@ -320,10 +321,26 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *config
|
||||||
Expression: validation.ErrorMessage,
|
Expression: validation.ErrorMessage,
|
||||||
EvalContext: hclCtx,
|
EvalContext: hclCtx,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
if marks.Has(errorValue, marks.Sensitive) {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
|
||||||
|
Summary: "Error message refers to sensitive values",
|
||||||
|
Detail: `The error expression used to explain this condition refers to sensitive values. Terraform will not display the resulting message.
|
||||||
|
|
||||||
|
You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`,
|
||||||
|
|
||||||
|
Subject: validation.ErrorMessage.Range().Ptr(),
|
||||||
|
Expression: validation.ErrorMessage,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
|
errorMessage = "The error message included a sensitive value, so it will not be displayed."
|
||||||
} else {
|
} else {
|
||||||
errorMessage = strings.TrimSpace(errorValue.AsString())
|
errorMessage = strings.TrimSpace(errorValue.AsString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if errorMessage == "" {
|
if errorMessage == "" {
|
||||||
errorMessage = "Failed to evaluate condition error message."
|
errorMessage = "Failed to evaluate condition error message."
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/lang"
|
"github.com/hashicorp/terraform/internal/lang"
|
||||||
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -715,3 +716,135 @@ func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEvalVariableValidations_sensitiveValues(t *testing.T) {
|
||||||
|
cfgSrc := `
|
||||||
|
variable "foo" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
default = "boop"
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = length(var.foo) == 4
|
||||||
|
error_message = "Foo must be 4 characters, not ${length(var.foo)}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "bar" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
default = "boop"
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = length(var.bar) == 4
|
||||||
|
error_message = "Bar must be 4 characters, not ${nonsensitive(length(var.bar))}."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
cfg := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": cfgSrc,
|
||||||
|
})
|
||||||
|
variableConfigs := cfg.Module.Variables
|
||||||
|
|
||||||
|
// Because we loaded our pseudo-module from a temporary file, the
|
||||||
|
// declaration source ranges will have unpredictable filenames. We'll
|
||||||
|
// fix that here just to make things easier below.
|
||||||
|
for _, vc := range variableConfigs {
|
||||||
|
vc.DeclRange.Filename = "main.tf"
|
||||||
|
for _, v := range vc.Validations {
|
||||||
|
v.DeclRange.Filename = "main.tf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
varName string
|
||||||
|
given cty.Value
|
||||||
|
wantErr []string
|
||||||
|
}{
|
||||||
|
// Validations pass on a sensitive variable with an error message which
|
||||||
|
// would generate a sensitive value
|
||||||
|
{
|
||||||
|
varName: "foo",
|
||||||
|
given: cty.StringVal("boop"),
|
||||||
|
},
|
||||||
|
// Assigning a value which fails the condition generates a sensitive
|
||||||
|
// error message, which is elided and generates another error
|
||||||
|
{
|
||||||
|
varName: "foo",
|
||||||
|
given: cty.StringVal("bap"),
|
||||||
|
wantErr: []string{
|
||||||
|
"Invalid value for variable",
|
||||||
|
"The error message included a sensitive value, so it will not be displayed.",
|
||||||
|
"Error message refers to sensitive values",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Validations pass on a sensitive variable with a correctly defined
|
||||||
|
// error message
|
||||||
|
{
|
||||||
|
varName: "bar",
|
||||||
|
given: cty.StringVal("boop"),
|
||||||
|
},
|
||||||
|
// Assigning a value which fails the condition generates a nonsensitive
|
||||||
|
// error message, which is displayed
|
||||||
|
{
|
||||||
|
varName: "bar",
|
||||||
|
given: cty.StringVal("bap"),
|
||||||
|
wantErr: []string{
|
||||||
|
"Invalid value for variable",
|
||||||
|
"Bar must be 4 characters, not 3.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
|
||||||
|
varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
|
||||||
|
varCfg := variableConfigs[test.varName]
|
||||||
|
if varCfg == nil {
|
||||||
|
t.Fatalf("invalid variable name %q", test.varName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a mock context to allow the function under test to
|
||||||
|
// retrieve the variable value and evaluate the expressions
|
||||||
|
ctx := &MockEvalContext{}
|
||||||
|
|
||||||
|
// We need a minimal scope to allow basic functions to be passed to
|
||||||
|
// the HCL scope
|
||||||
|
ctx.EvaluationScopeScope = &lang.Scope{}
|
||||||
|
ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
|
||||||
|
if got, want := addr.String(), varAddr.String(); got != want {
|
||||||
|
t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
if varCfg.Sensitive {
|
||||||
|
return test.given.Mark(marks.Sensitive)
|
||||||
|
} else {
|
||||||
|
return test.given
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gotDiags := evalVariableValidations(
|
||||||
|
varAddr, varCfg, nil, ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(test.wantErr) == 0 {
|
||||||
|
if len(gotDiags) > 0 {
|
||||||
|
t.Errorf("no diags expected, got %s", gotDiags.Err().Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wantErrs:
|
||||||
|
for _, want := range test.wantErr {
|
||||||
|
for _, diag := range gotDiags {
|
||||||
|
if diag.Severity() != tfdiags.Error {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
desc := diag.Description()
|
||||||
|
if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
|
||||||
|
continue wantErrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue