core: Fix sensitive variable validation errors
Variable validation error message expressions which generated sensitive values would previously crash. This commit updates the logic to align with preconditions and postconditions, eliding sensitive error message values and adding a separate diagnostic explaining why.
This commit is contained in:
parent
6db174e210
commit
b5cfc0bb8b
|
@ -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