diff --git a/internal/lang/funcs/collection.go b/internal/lang/funcs/collection.go index f9b3e6ae4..0272b2463 100644 --- a/internal/lang/funcs/collection.go +++ b/internal/lang/funcs/collection.go @@ -311,8 +311,8 @@ var LookupFunc = function.New(&function.Spec{ return defaultVal.WithMarks(markses...), nil } - return cty.UnknownVal(cty.DynamicPseudoType).WithMarks(markses...), fmt.Errorf( - "lookup failed to find '%s'", lookupKey) + return cty.UnknownVal(cty.DynamicPseudoType), fmt.Errorf( + "lookup failed to find key %s", redactIfSensitive(lookupKey, keyMarks)) }, }) diff --git a/internal/lang/funcs/collection_test.go b/internal/lang/funcs/collection_test.go index 3ca3f9181..2a1927acf 100644 --- a/internal/lang/funcs/collection_test.go +++ b/internal/lang/funcs/collection_test.go @@ -5,6 +5,7 @@ import ( "math" "testing" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/zclconf/go-cty/cty" ) @@ -899,6 +900,46 @@ func TestLookup(t *testing.T) { } } +func TestLookup_error(t *testing.T) { + simpleMap := cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }) + + tests := map[string]struct { + Values []cty.Value + WantErr string + }{ + "failed to find non-sensitive key": { + []cty.Value{ + simpleMap, + cty.StringVal("boop"), + }, + `lookup failed to find key "boop"`, + }, + "failed to find sensitive key": { + []cty.Value{ + simpleMap, + cty.StringVal("boop").Mark(marks.Sensitive), + }, + "lookup failed to find key (sensitive value)", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + _, err := Lookup(test.Values...) + + if err == nil { + t.Fatal("succeeded; want error") + } + + if err.Error() != test.WantErr { + t.Errorf("wrong error\ngot: %#v\nwant: %#v", err, test.WantErr) + } + }) + } +} + func TestMatchkeys(t *testing.T) { tests := []struct { Keys cty.Value diff --git a/internal/lang/funcs/encoding.go b/internal/lang/funcs/encoding.go index 27fc2a29c..2e67ebc8b 100644 --- a/internal/lang/funcs/encoding.go +++ b/internal/lang/funcs/encoding.go @@ -18,22 +18,24 @@ import ( var Base64DecodeFunc = function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "str", - Type: cty.String, + Name: "str", + Type: cty.String, + AllowMarked: true, }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - s := args[0].AsString() + str, strMarks := args[0].Unmark() + s := str.AsString() sDec, err := base64.StdEncoding.DecodeString(s) if err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data '%s'", s) + return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data %s", redactIfSensitive(s, strMarks)) } if !utf8.Valid([]byte(sDec)) { - log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", sDec) + log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", redactIfSensitive(sDec, strMarks)) return cty.UnknownVal(cty.String), fmt.Errorf("the result of decoding the provided string is not valid UTF-8") } - return cty.StringVal(string(sDec)), nil + return cty.StringVal(string(sDec)).WithMarks(strMarks), nil }, }) @@ -125,7 +127,7 @@ var TextDecodeBase64Func = function.New(&function.Spec{ case base64.CorruptInputError: return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given value is has an invalid base64 symbol at offset %d", int(err)) default: - return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %T", err) + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %w", err) } } @@ -156,13 +158,13 @@ var Base64GzipFunc = function.New(&function.Spec{ var b bytes.Buffer gz := gzip.NewWriter(&b) if _, err := gz.Write([]byte(s)); err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: '%s'", s) + return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: %w", err) } if err := gz.Flush(); err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: '%s'", s) + return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: %w", err) } if err := gz.Close(); err != nil { - return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: '%s'", s) + return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: %w", err) } return cty.StringVal(base64.StdEncoding.EncodeToString(b.Bytes())), nil }, diff --git a/internal/lang/funcs/encoding_test.go b/internal/lang/funcs/encoding_test.go index 2aa45a374..2e05784e8 100644 --- a/internal/lang/funcs/encoding_test.go +++ b/internal/lang/funcs/encoding_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/zclconf/go-cty/cty" ) @@ -18,6 +19,11 @@ func TestBase64Decode(t *testing.T) { cty.StringVal("abc123!?$*&()'-=@~"), false, }, + { + cty.StringVal("YWJjMTIzIT8kKiYoKSctPUB+").Mark(marks.Sensitive), + cty.StringVal("abc123!?$*&()'-=@~").Mark(marks.Sensitive), + false, + }, { // Invalid base64 data decoding cty.StringVal("this-is-an-invalid-base64-data"), cty.UnknownVal(cty.String), @@ -50,6 +56,40 @@ func TestBase64Decode(t *testing.T) { } } +func TestBase64Decode_error(t *testing.T) { + tests := map[string]struct { + String cty.Value + WantErr string + }{ + "invalid base64": { + cty.StringVal("dfg"), + `failed to decode base64 data "dfg"`, + }, + "sensitive invalid base64": { + cty.StringVal("dfg").Mark(marks.Sensitive), + `failed to decode base64 data (sensitive value)`, + }, + "invalid utf-8": { + cty.StringVal("whee"), + "the result of decoding the provided string is not valid UTF-8", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + _, err := Base64Decode(test.String) + + if err == nil { + t.Fatal("succeeded; want error") + } + + if err.Error() != test.WantErr { + t.Errorf("wrong error result\ngot: %#v\nwant: %#v", err.Error(), test.WantErr) + } + }) + } +} + func TestBase64Encode(t *testing.T) { tests := []struct { String cty.Value diff --git a/internal/lang/funcs/filesystem.go b/internal/lang/funcs/filesystem.go index 846b86110..01e090a5b 100644 --- a/internal/lang/funcs/filesystem.go +++ b/internal/lang/funcs/filesystem.go @@ -23,14 +23,16 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "path", - Type: cty.String, + Name: "path", + Type: cty.String, + AllowMarked: true, }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - path := args[0].AsString() - src, err := readFileBytes(baseDir, path) + pathArg, pathMarks := args[0].Unmark() + path := pathArg.AsString() + src, err := readFileBytes(baseDir, path, pathMarks) if err != nil { err = function.NewArgError(0, err) return cty.UnknownVal(cty.String), err @@ -39,12 +41,12 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function { switch { case encBase64: enc := base64.StdEncoding.EncodeToString(src) - return cty.StringVal(enc), nil + return cty.StringVal(enc).WithMarks(pathMarks), nil default: if !utf8.Valid(src) { - return cty.UnknownVal(cty.String), fmt.Errorf("contents of %s are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead", path) + return cty.UnknownVal(cty.String), fmt.Errorf("contents of %s are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead", redactIfSensitive(path, pathMarks)) } - return cty.StringVal(string(src)), nil + return cty.StringVal(string(src)).WithMarks(pathMarks), nil } }, }) @@ -67,8 +69,9 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Fun params := []function.Parameter{ { - Name: "path", - Type: cty.String, + Name: "path", + Type: cty.String, + AllowMarked: true, }, { Name: "vars", @@ -76,10 +79,10 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Fun }, } - loadTmpl := func(fn string) (hcl.Expression, error) { + loadTmpl := func(fn string, marks cty.ValueMarks) (hcl.Expression, error) { // We re-use File here to ensure the same filename interpretation // as it does, along with its other safety checks. - tmplVal, err := File(baseDir, cty.StringVal(fn)) + tmplVal, err := File(baseDir, cty.StringVal(fn).WithMarks(marks)) if err != nil { return nil, err } @@ -159,7 +162,9 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Fun // We'll render our template now to see what result type it produces. // A template consisting only of a single interpolation an potentially // return any type. - expr, err := loadTmpl(args[0].AsString()) + + pathArg, pathMarks := args[0].Unmark() + expr, err := loadTmpl(pathArg.AsString(), pathMarks) if err != nil { return cty.DynamicPseudoType, err } @@ -170,11 +175,13 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Fun return val.Type(), err }, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - expr, err := loadTmpl(args[0].AsString()) + pathArg, pathMarks := args[0].Unmark() + expr, err := loadTmpl(pathArg.AsString(), pathMarks) if err != nil { return cty.DynamicVal, err } - return renderTmpl(expr, args[1]) + result, err := renderTmpl(expr, args[1]) + return result.WithMarks(pathMarks), err }, }) @@ -186,16 +193,18 @@ func MakeFileExistsFunc(baseDir string) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "path", - Type: cty.String, + Name: "path", + Type: cty.String, + AllowMarked: true, }, }, Type: function.StaticReturnType(cty.Bool), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - path := args[0].AsString() + pathArg, pathMarks := args[0].Unmark() + path := pathArg.AsString() path, err := homedir.Expand(path) if err != nil { - return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to expand ~: %s", err) + return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to expand ~: %w", err) } if !filepath.IsAbs(path) { @@ -208,17 +217,17 @@ func MakeFileExistsFunc(baseDir string) function.Function { fi, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { - return cty.False, nil + return cty.False.WithMarks(pathMarks), nil } - return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to stat %s", path) + return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to stat %s", redactIfSensitive(path, pathMarks)) } if fi.Mode().IsRegular() { - return cty.True, nil + return cty.True.WithMarks(pathMarks), nil } return cty.False, fmt.Errorf("%s is not a regular file, but %q", - path, fi.Mode().String()) + redactIfSensitive(path, pathMarks), fi.Mode().String()) }, }) } @@ -229,18 +238,24 @@ func MakeFileSetFunc(baseDir string) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "path", - Type: cty.String, + Name: "path", + Type: cty.String, + AllowMarked: true, }, { - Name: "pattern", - Type: cty.String, + Name: "pattern", + Type: cty.String, + AllowMarked: true, }, }, Type: function.StaticReturnType(cty.Set(cty.String)), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - path := args[0].AsString() - pattern := args[1].AsString() + pathArg, pathMarks := args[0].Unmark() + path := pathArg.AsString() + patternArg, patternMarks := args[1].Unmark() + pattern := patternArg.AsString() + + marks := []cty.ValueMarks{pathMarks, patternMarks} if !filepath.IsAbs(path) { path = filepath.Join(baseDir, path) @@ -253,7 +268,7 @@ func MakeFileSetFunc(baseDir string) function.Function { matches, err := doublestar.Glob(pattern) if err != nil { - return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err) + return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern %s: %w", redactIfSensitive(pattern, marks...), err) } var matchVals []cty.Value @@ -261,7 +276,7 @@ func MakeFileSetFunc(baseDir string) function.Function { fi, err := os.Stat(match) if err != nil { - return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to stat (%s): %s", match, err) + return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to stat %s: %w", redactIfSensitive(match, marks...), err) } if !fi.Mode().IsRegular() { @@ -272,7 +287,7 @@ func MakeFileSetFunc(baseDir string) function.Function { match, err = filepath.Rel(path, match) if err != nil { - return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to trim path of match (%s): %s", match, err) + return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to trim path of match %s: %w", redactIfSensitive(match, marks...), err) } // Replace any remaining file separators with forward slash (/) @@ -283,10 +298,10 @@ func MakeFileSetFunc(baseDir string) function.Function { } if len(matchVals) == 0 { - return cty.SetValEmpty(cty.String), nil + return cty.SetValEmpty(cty.String).WithMarks(marks...), nil } - return cty.SetVal(matchVals), nil + return cty.SetVal(matchVals).WithMarks(marks...), nil }, }) } @@ -355,7 +370,7 @@ var PathExpandFunc = function.New(&function.Spec{ func openFile(baseDir, path string) (*os.File, error) { path, err := homedir.Expand(path) if err != nil { - return nil, fmt.Errorf("failed to expand ~: %s", err) + return nil, fmt.Errorf("failed to expand ~: %w", err) } if !filepath.IsAbs(path) { @@ -368,12 +383,12 @@ func openFile(baseDir, path string) (*os.File, error) { return os.Open(path) } -func readFileBytes(baseDir, path string) ([]byte, error) { +func readFileBytes(baseDir, path string, marks cty.ValueMarks) ([]byte, error) { f, err := openFile(baseDir, path) if err != nil { if os.IsNotExist(err) { // An extra Terraform-specific hint for this situation - return nil, fmt.Errorf("no file exists at %s; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource", path) + return nil, fmt.Errorf("no file exists at %s; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource", redactIfSensitive(path, marks)) } return nil, err } @@ -381,7 +396,7 @@ func readFileBytes(baseDir, path string) ([]byte, error) { src, err := ioutil.ReadAll(f) if err != nil { - return nil, fmt.Errorf("failed to read %s", path) + return nil, fmt.Errorf("failed to read file: %w", err) } return src, nil diff --git a/internal/lang/funcs/filesystem_test.go b/internal/lang/funcs/filesystem_test.go index b91b52b1e..4e673984b 100644 --- a/internal/lang/funcs/filesystem_test.go +++ b/internal/lang/funcs/filesystem_test.go @@ -2,9 +2,11 @@ package funcs import ( "fmt" + "os" "path/filepath" "testing" + "github.com/hashicorp/terraform/internal/lang/marks" homedir "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" @@ -15,22 +17,32 @@ func TestFile(t *testing.T) { tests := []struct { Path cty.Value Want cty.Value - Err bool + Err string }{ { cty.StringVal("testdata/hello.txt"), cty.StringVal("Hello World"), - false, + ``, }, { cty.StringVal("testdata/icon.png"), cty.NilVal, - true, // Not valid UTF-8 + `contents of "testdata/icon.png" are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`, + }, + { + cty.StringVal("testdata/icon.png").Mark(marks.Sensitive), + cty.NilVal, + `contents of (sensitive value) are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`, }, { cty.StringVal("testdata/missing"), cty.NilVal, - true, // no file exists + `no file exists at "testdata/missing"; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, + }, + { + cty.StringVal("testdata/missing").Mark(marks.Sensitive), + cty.NilVal, + `no file exists at (sensitive value); this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, }, } @@ -38,10 +50,13 @@ func TestFile(t *testing.T) { t.Run(fmt.Sprintf("File(\".\", %#v)", test.Path), func(t *testing.T) { got, err := File(".", test.Path) - if test.Err { + if test.Err != "" { if err == nil { t.Fatal("succeeded; want error") } + if got, want := err.Error(), test.Err; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } return } else if err != nil { t.Fatalf("unexpected error: %s", err) @@ -71,13 +86,19 @@ func TestTemplateFile(t *testing.T) { cty.StringVal("testdata/icon.png"), cty.EmptyObjectVal, cty.NilVal, - `contents of testdata/icon.png are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`, + `contents of "testdata/icon.png" are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`, }, { cty.StringVal("testdata/missing"), cty.EmptyObjectVal, cty.NilVal, - `no file exists at testdata/missing; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, + `no file exists at "testdata/missing"; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, + }, + { + cty.StringVal("testdata/secrets.txt").Mark(marks.Sensitive), + cty.EmptyObjectVal, + cty.NilVal, + `no file exists at (sensitive value); this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, }, { cty.StringVal("testdata/hello.tmpl"), @@ -197,33 +218,61 @@ func TestFileExists(t *testing.T) { tests := []struct { Path cty.Value Want cty.Value - Err bool + Err string }{ { cty.StringVal("testdata/hello.txt"), cty.BoolVal(true), - false, + ``, }, { - cty.StringVal(""), // empty path + cty.StringVal(""), cty.BoolVal(false), - true, + `"." is not a regular file, but "drwxr-xr-x"`, + }, + { + cty.StringVal("testdata").Mark(marks.Sensitive), + cty.BoolVal(false), + `(sensitive value) is not a regular file, but "drwxr-xr-x"`, }, { cty.StringVal("testdata/missing"), cty.BoolVal(false), - false, // no file exists + ``, + }, + { + cty.StringVal("testdata/unreadable/foobar"), + cty.BoolVal(false), + `failed to stat "testdata/unreadable/foobar"`, + }, + { + cty.StringVal("testdata/unreadable/foobar").Mark(marks.Sensitive), + cty.BoolVal(false), + `failed to stat (sensitive value)`, }, } + // Ensure "unreadable" directory cannot be listed during the test run + fi, err := os.Lstat("testdata/unreadable") + if err != nil { + t.Fatal(err) + } + os.Chmod("testdata/unreadable", 0000) + defer func(mode os.FileMode) { + os.Chmod("testdata/unreadable", mode) + }(fi.Mode()) + for _, test := range tests { t.Run(fmt.Sprintf("FileExists(\".\", %#v)", test.Path), func(t *testing.T) { got, err := FileExists(".", test.Path) - if test.Err { + if test.Err != "" { if err == nil { t.Fatal("succeeded; want error") } + if got, want := err.Error(), test.Err; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } return } else if err != nil { t.Fatalf("unexpected error: %s", err) @@ -241,49 +290,49 @@ func TestFileSet(t *testing.T) { Path cty.Value Pattern cty.Value Want cty.Value - Err bool + Err string }{ { cty.StringVal("."), cty.StringVal("testdata*"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("."), cty.StringVal("testdata"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("."), cty.StringVal("{testdata,missing}"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("."), cty.StringVal("testdata/missing"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("."), cty.StringVal("testdata/missing*"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("."), cty.StringVal("*/missing"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("."), cty.StringVal("**/missing"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("."), @@ -291,7 +340,7 @@ func TestFileSet(t *testing.T) { cty.SetVal([]cty.Value{ cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -299,7 +348,7 @@ func TestFileSet(t *testing.T) { cty.SetVal([]cty.Value{ cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -307,7 +356,7 @@ func TestFileSet(t *testing.T) { cty.SetVal([]cty.Value{ cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -316,7 +365,7 @@ func TestFileSet(t *testing.T) { cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -325,7 +374,7 @@ func TestFileSet(t *testing.T) { cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -333,7 +382,7 @@ func TestFileSet(t *testing.T) { cty.SetVal([]cty.Value{ cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -341,7 +390,7 @@ func TestFileSet(t *testing.T) { cty.SetVal([]cty.Value{ cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -350,7 +399,7 @@ func TestFileSet(t *testing.T) { cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -359,7 +408,7 @@ func TestFileSet(t *testing.T) { cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), @@ -368,31 +417,37 @@ func TestFileSet(t *testing.T) { cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.txt"), }), - false, + ``, }, { cty.StringVal("."), cty.StringVal("["), cty.SetValEmpty(cty.String), - true, + `failed to glob pattern "[": syntax error in pattern`, + }, + { + cty.StringVal("."), + cty.StringVal("[").Mark(marks.Sensitive), + cty.SetValEmpty(cty.String), + `failed to glob pattern (sensitive value): syntax error in pattern`, }, { cty.StringVal("."), cty.StringVal("\\"), cty.SetValEmpty(cty.String), - true, + `failed to glob pattern "\\": syntax error in pattern`, }, { cty.StringVal("testdata"), cty.StringVal("missing"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("testdata"), cty.StringVal("missing*"), cty.SetValEmpty(cty.String), - false, + ``, }, { cty.StringVal("testdata"), @@ -400,7 +455,7 @@ func TestFileSet(t *testing.T) { cty.SetVal([]cty.Value{ cty.StringVal("hello.txt"), }), - false, + ``, }, { cty.StringVal("testdata"), @@ -408,7 +463,7 @@ func TestFileSet(t *testing.T) { cty.SetVal([]cty.Value{ cty.StringVal("hello.txt"), }), - false, + ``, }, { cty.StringVal("testdata"), @@ -416,7 +471,7 @@ func TestFileSet(t *testing.T) { cty.SetVal([]cty.Value{ cty.StringVal("hello.txt"), }), - false, + ``, }, { cty.StringVal("testdata"), @@ -425,7 +480,7 @@ func TestFileSet(t *testing.T) { cty.StringVal("hello.tmpl"), cty.StringVal("hello.txt"), }), - false, + ``, }, } @@ -433,10 +488,13 @@ func TestFileSet(t *testing.T) { t.Run(fmt.Sprintf("FileSet(\".\", %#v, %#v)", test.Path, test.Pattern), func(t *testing.T) { got, err := FileSet(".", test.Path, test.Pattern) - if test.Err { + if test.Err != "" { if err == nil { t.Fatal("succeeded; want error") } + if got, want := err.Error(), test.Err; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } return } else if err != nil { t.Fatalf("unexpected error: %s", err) diff --git a/internal/lang/funcs/number.go b/internal/lang/funcs/number.go index 43effec12..d95870610 100644 --- a/internal/lang/funcs/number.go +++ b/internal/lang/funcs/number.go @@ -95,12 +95,14 @@ var SignumFunc = function.New(&function.Spec{ var ParseIntFunc = function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "number", - Type: cty.DynamicPseudoType, + Name: "number", + Type: cty.DynamicPseudoType, + AllowMarked: true, }, { - Name: "base", - Type: cty.Number, + Name: "base", + Type: cty.Number, + AllowMarked: true, }, }, @@ -116,11 +118,13 @@ var ParseIntFunc = function.New(&function.Spec{ var base int var err error - if err = gocty.FromCtyValue(args[0], &numstr); err != nil { + numArg, numMarks := args[0].Unmark() + if err = gocty.FromCtyValue(numArg, &numstr); err != nil { return cty.UnknownVal(cty.String), function.NewArgError(0, err) } - if err = gocty.FromCtyValue(args[1], &base); err != nil { + baseArg, baseMarks := args[1].Unmark() + if err = gocty.FromCtyValue(baseArg, &base); err != nil { return cty.UnknownVal(cty.Number), function.NewArgError(1, err) } @@ -135,13 +139,13 @@ var ParseIntFunc = function.New(&function.Spec{ if !ok { return cty.UnknownVal(cty.Number), function.NewArgErrorf( 0, - "cannot parse %q as a base %d integer", - numstr, - base, + "cannot parse %s as a base %s integer", + redactIfSensitive(numstr, numMarks), + redactIfSensitive(base, baseMarks), ) } - parsedNum := cty.NumberVal((&big.Float{}).SetInt(num)) + parsedNum := cty.NumberVal((&big.Float{}).SetInt(num)).WithMarks(numMarks, baseMarks) return parsedNum, nil }, diff --git a/internal/lang/funcs/number_test.go b/internal/lang/funcs/number_test.go index b467a429f..260e0127c 100644 --- a/internal/lang/funcs/number_test.go +++ b/internal/lang/funcs/number_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/zclconf/go-cty/cty" ) @@ -187,139 +188,175 @@ func TestParseInt(t *testing.T) { Num cty.Value Base cty.Value Want cty.Value - Err bool + Err string }{ { cty.StringVal("128"), cty.NumberIntVal(10), cty.NumberIntVal(128), - false, + ``, + }, + { + cty.StringVal("128").Mark(marks.Sensitive), + cty.NumberIntVal(10), + cty.NumberIntVal(128).Mark(marks.Sensitive), + ``, + }, + { + cty.StringVal("128"), + cty.NumberIntVal(10).Mark(marks.Sensitive), + cty.NumberIntVal(128).Mark(marks.Sensitive), + ``, + }, + { + cty.StringVal("128").Mark(marks.Sensitive), + cty.NumberIntVal(10).Mark(marks.Sensitive), + cty.NumberIntVal(128).Mark(marks.Sensitive), + ``, + }, + { + cty.StringVal("128").Mark(marks.Raw), + cty.NumberIntVal(10).Mark(marks.Sensitive), + cty.NumberIntVal(128).WithMarks(cty.NewValueMarks(marks.Raw, marks.Sensitive)), + ``, }, { cty.StringVal("-128"), cty.NumberIntVal(10), cty.NumberIntVal(-128), - false, + ``, }, { cty.StringVal("00128"), cty.NumberIntVal(10), cty.NumberIntVal(128), - false, + ``, }, { cty.StringVal("-00128"), cty.NumberIntVal(10), cty.NumberIntVal(-128), - false, + ``, }, { cty.StringVal("FF00"), cty.NumberIntVal(16), cty.NumberIntVal(65280), - false, + ``, }, { cty.StringVal("ff00"), cty.NumberIntVal(16), cty.NumberIntVal(65280), - false, + ``, }, { cty.StringVal("-FF00"), cty.NumberIntVal(16), cty.NumberIntVal(-65280), - false, + ``, }, { cty.StringVal("00FF00"), cty.NumberIntVal(16), cty.NumberIntVal(65280), - false, + ``, }, { cty.StringVal("-00FF00"), cty.NumberIntVal(16), cty.NumberIntVal(-65280), - false, + ``, }, { cty.StringVal("1011111011101111"), cty.NumberIntVal(2), cty.NumberIntVal(48879), - false, + ``, }, { cty.StringVal("aA"), cty.NumberIntVal(62), cty.NumberIntVal(656), - false, + ``, }, { cty.StringVal("Aa"), cty.NumberIntVal(62), cty.NumberIntVal(2242), - false, + ``, }, { cty.StringVal("999999999999999999999999999999999999999999999999999999999999"), cty.NumberIntVal(10), cty.MustParseNumberVal("999999999999999999999999999999999999999999999999999999999999"), - false, + ``, }, { cty.StringVal("FF"), cty.NumberIntVal(10), cty.UnknownVal(cty.Number), - true, + `cannot parse "FF" as a base 10 integer`, + }, + { + cty.StringVal("FF").Mark(marks.Sensitive), + cty.NumberIntVal(10), + cty.UnknownVal(cty.Number), + `cannot parse (sensitive value) as a base 10 integer`, + }, + { + cty.StringVal("FF").Mark(marks.Sensitive), + cty.NumberIntVal(10).Mark(marks.Sensitive), + cty.UnknownVal(cty.Number), + `cannot parse (sensitive value) as a base (sensitive value) integer`, }, { cty.StringVal("00FF"), cty.NumberIntVal(10), cty.UnknownVal(cty.Number), - true, + `cannot parse "00FF" as a base 10 integer`, }, { cty.StringVal("-00FF"), cty.NumberIntVal(10), cty.UnknownVal(cty.Number), - true, + `cannot parse "-00FF" as a base 10 integer`, }, { cty.NumberIntVal(2), cty.NumberIntVal(10), cty.UnknownVal(cty.Number), - true, + `first argument must be a string, not number`, }, { cty.StringVal("1"), cty.NumberIntVal(63), cty.UnknownVal(cty.Number), - true, + `base must be a whole number between 2 and 62 inclusive`, }, { cty.StringVal("1"), cty.NumberIntVal(-1), cty.UnknownVal(cty.Number), - true, + `base must be a whole number between 2 and 62 inclusive`, }, { cty.StringVal("1"), cty.NumberIntVal(1), cty.UnknownVal(cty.Number), - true, + `base must be a whole number between 2 and 62 inclusive`, }, { cty.StringVal("1"), cty.NumberIntVal(0), cty.UnknownVal(cty.Number), - true, + `base must be a whole number between 2 and 62 inclusive`, }, { cty.StringVal("1.2"), cty.NumberIntVal(10), cty.UnknownVal(cty.Number), - true, + `cannot parse "1.2" as a base 10 integer`, }, } @@ -327,10 +364,13 @@ func TestParseInt(t *testing.T) { t.Run(fmt.Sprintf("parseint(%#v, %#v)", test.Num, test.Base), func(t *testing.T) { got, err := ParseInt(test.Num, test.Base) - if test.Err { + if test.Err != "" { if err == nil { t.Fatal("succeeded; want error") } + if got, want := err.Error(), test.Err; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } return } else if err != nil { t.Fatalf("unexpected error: %s", err) diff --git a/internal/lang/funcs/redact.go b/internal/lang/funcs/redact.go new file mode 100644 index 000000000..bbec3f0a1 --- /dev/null +++ b/internal/lang/funcs/redact.go @@ -0,0 +1,20 @@ +package funcs + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/zclconf/go-cty/cty" +) + +func redactIfSensitive(value interface{}, markses ...cty.ValueMarks) string { + if marks.Has(cty.DynamicVal.WithMarks(markses...), marks.Sensitive) { + return "(sensitive value)" + } + switch v := value.(type) { + case string: + return fmt.Sprintf("%q", v) + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/internal/lang/funcs/redact_test.go b/internal/lang/funcs/redact_test.go new file mode 100644 index 000000000..b45721fb9 --- /dev/null +++ b/internal/lang/funcs/redact_test.go @@ -0,0 +1,51 @@ +package funcs + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/zclconf/go-cty/cty" +) + +func TestRedactIfSensitive(t *testing.T) { + testCases := map[string]struct { + value interface{} + marks []cty.ValueMarks + want string + }{ + "sensitive string": { + value: "foo", + marks: []cty.ValueMarks{cty.NewValueMarks(marks.Sensitive)}, + want: "(sensitive value)", + }, + "raw non-sensitive string": { + value: "foo", + marks: []cty.ValueMarks{cty.NewValueMarks(marks.Raw)}, + want: `"foo"`, + }, + "raw sensitive string": { + value: "foo", + marks: []cty.ValueMarks{cty.NewValueMarks(marks.Raw), cty.NewValueMarks(marks.Sensitive)}, + want: "(sensitive value)", + }, + "sensitive number": { + value: 12345, + marks: []cty.ValueMarks{cty.NewValueMarks(marks.Sensitive)}, + want: "(sensitive value)", + }, + "non-sensitive number": { + value: 12345, + marks: []cty.ValueMarks{}, + want: "12345", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := redactIfSensitive(tc.value, tc.marks...) + if got != tc.want { + t.Errorf("wrong result, got %v, want %v", got, tc.want) + } + }) + } +} diff --git a/internal/lang/funcs/testdata/unreadable/foobar b/internal/lang/funcs/testdata/unreadable/foobar new file mode 100644 index 000000000..e69de29bb