diff --git a/internal/terraform/eval_variable.go b/internal/terraform/eval_variable.go index 629da7051..f6c927e5e 100644 --- a/internal/terraform/eval_variable.go +++ b/internal/terraform/eval_variable.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" @@ -321,7 +322,23 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *config EvalContext: hclCtx, }) } else { - errorMessage = strings.TrimSpace(errorValue.AsString()) + 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 { + errorMessage = strings.TrimSpace(errorValue.AsString()) + } } } if errorMessage == "" { diff --git a/internal/terraform/eval_variable_test.go b/internal/terraform/eval_variable_test.go index c84a969e8..662bf5626 100644 --- a/internal/terraform/eval_variable_test.go +++ b/internal/terraform/eval_variable_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" "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()) + } + } + }) + } +}