lang: Redact sensitive values from function errors

Some function errors include values derived from arguments. This commit
is the result of a manual audit of these errors, which resulted in:

- Adding a helper function to redact sensitive values;
- Applying that helper function where errors include values derived from
  possibly-sensitive arguments;
- Cleaning up other errors which need not include those values, or were
  otherwise incorrect.
This commit is contained in:
Alisdair McDiarmid 2021-12-01 13:10:54 -05:00
parent ba6a64eb35
commit 5d7cb81c0c
11 changed files with 394 additions and 123 deletions

View File

@ -311,8 +311,8 @@ var LookupFunc = function.New(&function.Spec{
return defaultVal.WithMarks(markses...), nil return defaultVal.WithMarks(markses...), nil
} }
return cty.UnknownVal(cty.DynamicPseudoType).WithMarks(markses...), fmt.Errorf( return cty.UnknownVal(cty.DynamicPseudoType), fmt.Errorf(
"lookup failed to find '%s'", lookupKey) "lookup failed to find key %s", redactIfSensitive(lookupKey, keyMarks))
}, },
}) })

View File

@ -5,6 +5,7 @@ import (
"math" "math"
"testing" "testing"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/zclconf/go-cty/cty" "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) { func TestMatchkeys(t *testing.T) {
tests := []struct { tests := []struct {
Keys cty.Value Keys cty.Value

View File

@ -18,22 +18,24 @@ import (
var Base64DecodeFunc = function.New(&function.Spec{ var Base64DecodeFunc = function.New(&function.Spec{
Params: []function.Parameter{ Params: []function.Parameter{
{ {
Name: "str", Name: "str",
Type: cty.String, Type: cty.String,
AllowMarked: true,
}, },
}, },
Type: function.StaticReturnType(cty.String), Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 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) sDec, err := base64.StdEncoding.DecodeString(s)
if err != nil { 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)) { 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.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: 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)) return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given value is has an invalid base64 symbol at offset %d", int(err))
default: 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 var b bytes.Buffer
gz := gzip.NewWriter(&b) gz := gzip.NewWriter(&b)
if _, err := gz.Write([]byte(s)); err != nil { 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 { 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 { 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 return cty.StringVal(base64.StdEncoding.EncodeToString(b.Bytes())), nil
}, },

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -18,6 +19,11 @@ func TestBase64Decode(t *testing.T) {
cty.StringVal("abc123!?$*&()'-=@~"), cty.StringVal("abc123!?$*&()'-=@~"),
false, false,
}, },
{
cty.StringVal("YWJjMTIzIT8kKiYoKSctPUB+").Mark(marks.Sensitive),
cty.StringVal("abc123!?$*&()'-=@~").Mark(marks.Sensitive),
false,
},
{ // Invalid base64 data decoding { // Invalid base64 data decoding
cty.StringVal("this-is-an-invalid-base64-data"), cty.StringVal("this-is-an-invalid-base64-data"),
cty.UnknownVal(cty.String), 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) { func TestBase64Encode(t *testing.T) {
tests := []struct { tests := []struct {
String cty.Value String cty.Value

View File

@ -23,14 +23,16 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function {
return function.New(&function.Spec{ return function.New(&function.Spec{
Params: []function.Parameter{ Params: []function.Parameter{
{ {
Name: "path", Name: "path",
Type: cty.String, Type: cty.String,
AllowMarked: true,
}, },
}, },
Type: function.StaticReturnType(cty.String), Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
path := args[0].AsString() pathArg, pathMarks := args[0].Unmark()
src, err := readFileBytes(baseDir, path) path := pathArg.AsString()
src, err := readFileBytes(baseDir, path, pathMarks)
if err != nil { if err != nil {
err = function.NewArgError(0, err) err = function.NewArgError(0, err)
return cty.UnknownVal(cty.String), err return cty.UnknownVal(cty.String), err
@ -39,12 +41,12 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function {
switch { switch {
case encBase64: case encBase64:
enc := base64.StdEncoding.EncodeToString(src) enc := base64.StdEncoding.EncodeToString(src)
return cty.StringVal(enc), nil return cty.StringVal(enc).WithMarks(pathMarks), nil
default: default:
if !utf8.Valid(src) { 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{ params := []function.Parameter{
{ {
Name: "path", Name: "path",
Type: cty.String, Type: cty.String,
AllowMarked: true,
}, },
{ {
Name: "vars", 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 // We re-use File here to ensure the same filename interpretation
// as it does, along with its other safety checks. // 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 { if err != nil {
return nil, err 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. // We'll render our template now to see what result type it produces.
// A template consisting only of a single interpolation an potentially // A template consisting only of a single interpolation an potentially
// return any type. // return any type.
expr, err := loadTmpl(args[0].AsString())
pathArg, pathMarks := args[0].Unmark()
expr, err := loadTmpl(pathArg.AsString(), pathMarks)
if err != nil { if err != nil {
return cty.DynamicPseudoType, err return cty.DynamicPseudoType, err
} }
@ -170,11 +175,13 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Fun
return val.Type(), err return val.Type(), err
}, },
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 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 { if err != nil {
return cty.DynamicVal, err 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{ return function.New(&function.Spec{
Params: []function.Parameter{ Params: []function.Parameter{
{ {
Name: "path", Name: "path",
Type: cty.String, Type: cty.String,
AllowMarked: true,
}, },
}, },
Type: function.StaticReturnType(cty.Bool), Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 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) path, err := homedir.Expand(path)
if err != nil { 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) { if !filepath.IsAbs(path) {
@ -208,17 +217,17 @@ func MakeFileExistsFunc(baseDir string) function.Function {
fi, err := os.Stat(path) fi, err := os.Stat(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { 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() { 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", 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{ return function.New(&function.Spec{
Params: []function.Parameter{ Params: []function.Parameter{
{ {
Name: "path", Name: "path",
Type: cty.String, Type: cty.String,
AllowMarked: true,
}, },
{ {
Name: "pattern", Name: "pattern",
Type: cty.String, Type: cty.String,
AllowMarked: true,
}, },
}, },
Type: function.StaticReturnType(cty.Set(cty.String)), Type: function.StaticReturnType(cty.Set(cty.String)),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
path := args[0].AsString() pathArg, pathMarks := args[0].Unmark()
pattern := args[1].AsString() path := pathArg.AsString()
patternArg, patternMarks := args[1].Unmark()
pattern := patternArg.AsString()
marks := []cty.ValueMarks{pathMarks, patternMarks}
if !filepath.IsAbs(path) { if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path) path = filepath.Join(baseDir, path)
@ -253,7 +268,7 @@ func MakeFileSetFunc(baseDir string) function.Function {
matches, err := doublestar.Glob(pattern) matches, err := doublestar.Glob(pattern)
if err != nil { 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 var matchVals []cty.Value
@ -261,7 +276,7 @@ func MakeFileSetFunc(baseDir string) function.Function {
fi, err := os.Stat(match) fi, err := os.Stat(match)
if err != nil { 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() { if !fi.Mode().IsRegular() {
@ -272,7 +287,7 @@ func MakeFileSetFunc(baseDir string) function.Function {
match, err = filepath.Rel(path, match) match, err = filepath.Rel(path, match)
if err != nil { 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 (/) // Replace any remaining file separators with forward slash (/)
@ -283,10 +298,10 @@ func MakeFileSetFunc(baseDir string) function.Function {
} }
if len(matchVals) == 0 { 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) { func openFile(baseDir, path string) (*os.File, error) {
path, err := homedir.Expand(path) path, err := homedir.Expand(path)
if err != nil { 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) { if !filepath.IsAbs(path) {
@ -368,12 +383,12 @@ func openFile(baseDir, path string) (*os.File, error) {
return os.Open(path) 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) f, err := openFile(baseDir, path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
// An extra Terraform-specific hint for this situation // 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 return nil, err
} }
@ -381,7 +396,7 @@ func readFileBytes(baseDir, path string) ([]byte, error) {
src, err := ioutil.ReadAll(f) src, err := ioutil.ReadAll(f)
if err != nil { 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 return src, nil

View File

@ -2,9 +2,11 @@ package funcs
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/hashicorp/terraform/internal/lang/marks"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/function"
@ -15,22 +17,32 @@ func TestFile(t *testing.T) {
tests := []struct { tests := []struct {
Path cty.Value Path cty.Value
Want cty.Value Want cty.Value
Err bool Err string
}{ }{
{ {
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
cty.StringVal("Hello World"), cty.StringVal("Hello World"),
false, ``,
}, },
{ {
cty.StringVal("testdata/icon.png"), cty.StringVal("testdata/icon.png"),
cty.NilVal, 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.StringVal("testdata/missing"),
cty.NilVal, 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) { t.Run(fmt.Sprintf("File(\".\", %#v)", test.Path), func(t *testing.T) {
got, err := File(".", test.Path) got, err := File(".", test.Path)
if test.Err { if test.Err != "" {
if err == nil { if err == nil {
t.Fatal("succeeded; want error") 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 return
} else if err != nil { } else if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
@ -71,13 +86,19 @@ func TestTemplateFile(t *testing.T) {
cty.StringVal("testdata/icon.png"), cty.StringVal("testdata/icon.png"),
cty.EmptyObjectVal, cty.EmptyObjectVal,
cty.NilVal, 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.StringVal("testdata/missing"),
cty.EmptyObjectVal, cty.EmptyObjectVal,
cty.NilVal, 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"), cty.StringVal("testdata/hello.tmpl"),
@ -197,33 +218,61 @@ func TestFileExists(t *testing.T) {
tests := []struct { tests := []struct {
Path cty.Value Path cty.Value
Want cty.Value Want cty.Value
Err bool Err string
}{ }{
{ {
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
cty.BoolVal(true), cty.BoolVal(true),
false, ``,
}, },
{ {
cty.StringVal(""), // empty path cty.StringVal(""),
cty.BoolVal(false), 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.StringVal("testdata/missing"),
cty.BoolVal(false), 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 { for _, test := range tests {
t.Run(fmt.Sprintf("FileExists(\".\", %#v)", test.Path), func(t *testing.T) { t.Run(fmt.Sprintf("FileExists(\".\", %#v)", test.Path), func(t *testing.T) {
got, err := FileExists(".", test.Path) got, err := FileExists(".", test.Path)
if test.Err { if test.Err != "" {
if err == nil { if err == nil {
t.Fatal("succeeded; want error") 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 return
} else if err != nil { } else if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
@ -241,49 +290,49 @@ func TestFileSet(t *testing.T) {
Path cty.Value Path cty.Value
Pattern cty.Value Pattern cty.Value
Want cty.Value Want cty.Value
Err bool Err string
}{ }{
{ {
cty.StringVal("."), cty.StringVal("."),
cty.StringVal("testdata*"), cty.StringVal("testdata*"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
cty.StringVal("testdata"), cty.StringVal("testdata"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
cty.StringVal("{testdata,missing}"), cty.StringVal("{testdata,missing}"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
cty.StringVal("testdata/missing"), cty.StringVal("testdata/missing"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
cty.StringVal("testdata/missing*"), cty.StringVal("testdata/missing*"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
cty.StringVal("*/missing"), cty.StringVal("*/missing"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
cty.StringVal("**/missing"), cty.StringVal("**/missing"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -291,7 +340,7 @@ func TestFileSet(t *testing.T) {
cty.SetVal([]cty.Value{ cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -299,7 +348,7 @@ func TestFileSet(t *testing.T) {
cty.SetVal([]cty.Value{ cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -307,7 +356,7 @@ func TestFileSet(t *testing.T) {
cty.SetVal([]cty.Value{ cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -316,7 +365,7 @@ func TestFileSet(t *testing.T) {
cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.tmpl"),
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -325,7 +374,7 @@ func TestFileSet(t *testing.T) {
cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.tmpl"),
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -333,7 +382,7 @@ func TestFileSet(t *testing.T) {
cty.SetVal([]cty.Value{ cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -341,7 +390,7 @@ func TestFileSet(t *testing.T) {
cty.SetVal([]cty.Value{ cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -350,7 +399,7 @@ func TestFileSet(t *testing.T) {
cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.tmpl"),
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -359,7 +408,7 @@ func TestFileSet(t *testing.T) {
cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.tmpl"),
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
@ -368,31 +417,37 @@ func TestFileSet(t *testing.T) {
cty.StringVal("testdata/hello.tmpl"), cty.StringVal("testdata/hello.tmpl"),
cty.StringVal("testdata/hello.txt"), cty.StringVal("testdata/hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("."), cty.StringVal("."),
cty.StringVal("["), cty.StringVal("["),
cty.SetValEmpty(cty.String), 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.StringVal("\\"), cty.StringVal("\\"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
true, `failed to glob pattern "\\": syntax error in pattern`,
}, },
{ {
cty.StringVal("testdata"), cty.StringVal("testdata"),
cty.StringVal("missing"), cty.StringVal("missing"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("testdata"), cty.StringVal("testdata"),
cty.StringVal("missing*"), cty.StringVal("missing*"),
cty.SetValEmpty(cty.String), cty.SetValEmpty(cty.String),
false, ``,
}, },
{ {
cty.StringVal("testdata"), cty.StringVal("testdata"),
@ -400,7 +455,7 @@ func TestFileSet(t *testing.T) {
cty.SetVal([]cty.Value{ cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"), cty.StringVal("hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("testdata"), cty.StringVal("testdata"),
@ -408,7 +463,7 @@ func TestFileSet(t *testing.T) {
cty.SetVal([]cty.Value{ cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"), cty.StringVal("hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("testdata"), cty.StringVal("testdata"),
@ -416,7 +471,7 @@ func TestFileSet(t *testing.T) {
cty.SetVal([]cty.Value{ cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"), cty.StringVal("hello.txt"),
}), }),
false, ``,
}, },
{ {
cty.StringVal("testdata"), cty.StringVal("testdata"),
@ -425,7 +480,7 @@ func TestFileSet(t *testing.T) {
cty.StringVal("hello.tmpl"), cty.StringVal("hello.tmpl"),
cty.StringVal("hello.txt"), 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) { t.Run(fmt.Sprintf("FileSet(\".\", %#v, %#v)", test.Path, test.Pattern), func(t *testing.T) {
got, err := FileSet(".", test.Path, test.Pattern) got, err := FileSet(".", test.Path, test.Pattern)
if test.Err { if test.Err != "" {
if err == nil { if err == nil {
t.Fatal("succeeded; want error") 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 return
} else if err != nil { } else if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)

View File

@ -95,12 +95,14 @@ var SignumFunc = function.New(&function.Spec{
var ParseIntFunc = function.New(&function.Spec{ var ParseIntFunc = function.New(&function.Spec{
Params: []function.Parameter{ Params: []function.Parameter{
{ {
Name: "number", Name: "number",
Type: cty.DynamicPseudoType, Type: cty.DynamicPseudoType,
AllowMarked: true,
}, },
{ {
Name: "base", Name: "base",
Type: cty.Number, Type: cty.Number,
AllowMarked: true,
}, },
}, },
@ -116,11 +118,13 @@ var ParseIntFunc = function.New(&function.Spec{
var base int var base int
var err error 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) 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) return cty.UnknownVal(cty.Number), function.NewArgError(1, err)
} }
@ -135,13 +139,13 @@ var ParseIntFunc = function.New(&function.Spec{
if !ok { if !ok {
return cty.UnknownVal(cty.Number), function.NewArgErrorf( return cty.UnknownVal(cty.Number), function.NewArgErrorf(
0, 0,
"cannot parse %q as a base %d integer", "cannot parse %s as a base %s integer",
numstr, redactIfSensitive(numstr, numMarks),
base, redactIfSensitive(base, baseMarks),
) )
} }
parsedNum := cty.NumberVal((&big.Float{}).SetInt(num)) parsedNum := cty.NumberVal((&big.Float{}).SetInt(num)).WithMarks(numMarks, baseMarks)
return parsedNum, nil return parsedNum, nil
}, },

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -187,139 +188,175 @@ func TestParseInt(t *testing.T) {
Num cty.Value Num cty.Value
Base cty.Value Base cty.Value
Want cty.Value Want cty.Value
Err bool Err string
}{ }{
{ {
cty.StringVal("128"), cty.StringVal("128"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.NumberIntVal(128), 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.StringVal("-128"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.NumberIntVal(-128), cty.NumberIntVal(-128),
false, ``,
}, },
{ {
cty.StringVal("00128"), cty.StringVal("00128"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.NumberIntVal(128), cty.NumberIntVal(128),
false, ``,
}, },
{ {
cty.StringVal("-00128"), cty.StringVal("-00128"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.NumberIntVal(-128), cty.NumberIntVal(-128),
false, ``,
}, },
{ {
cty.StringVal("FF00"), cty.StringVal("FF00"),
cty.NumberIntVal(16), cty.NumberIntVal(16),
cty.NumberIntVal(65280), cty.NumberIntVal(65280),
false, ``,
}, },
{ {
cty.StringVal("ff00"), cty.StringVal("ff00"),
cty.NumberIntVal(16), cty.NumberIntVal(16),
cty.NumberIntVal(65280), cty.NumberIntVal(65280),
false, ``,
}, },
{ {
cty.StringVal("-FF00"), cty.StringVal("-FF00"),
cty.NumberIntVal(16), cty.NumberIntVal(16),
cty.NumberIntVal(-65280), cty.NumberIntVal(-65280),
false, ``,
}, },
{ {
cty.StringVal("00FF00"), cty.StringVal("00FF00"),
cty.NumberIntVal(16), cty.NumberIntVal(16),
cty.NumberIntVal(65280), cty.NumberIntVal(65280),
false, ``,
}, },
{ {
cty.StringVal("-00FF00"), cty.StringVal("-00FF00"),
cty.NumberIntVal(16), cty.NumberIntVal(16),
cty.NumberIntVal(-65280), cty.NumberIntVal(-65280),
false, ``,
}, },
{ {
cty.StringVal("1011111011101111"), cty.StringVal("1011111011101111"),
cty.NumberIntVal(2), cty.NumberIntVal(2),
cty.NumberIntVal(48879), cty.NumberIntVal(48879),
false, ``,
}, },
{ {
cty.StringVal("aA"), cty.StringVal("aA"),
cty.NumberIntVal(62), cty.NumberIntVal(62),
cty.NumberIntVal(656), cty.NumberIntVal(656),
false, ``,
}, },
{ {
cty.StringVal("Aa"), cty.StringVal("Aa"),
cty.NumberIntVal(62), cty.NumberIntVal(62),
cty.NumberIntVal(2242), cty.NumberIntVal(2242),
false, ``,
}, },
{ {
cty.StringVal("999999999999999999999999999999999999999999999999999999999999"), cty.StringVal("999999999999999999999999999999999999999999999999999999999999"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.MustParseNumberVal("999999999999999999999999999999999999999999999999999999999999"), cty.MustParseNumberVal("999999999999999999999999999999999999999999999999999999999999"),
false, ``,
}, },
{ {
cty.StringVal("FF"), cty.StringVal("FF"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.UnknownVal(cty.Number), 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.StringVal("00FF"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.UnknownVal(cty.Number), cty.UnknownVal(cty.Number),
true, `cannot parse "00FF" as a base 10 integer`,
}, },
{ {
cty.StringVal("-00FF"), cty.StringVal("-00FF"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.UnknownVal(cty.Number), cty.UnknownVal(cty.Number),
true, `cannot parse "-00FF" as a base 10 integer`,
}, },
{ {
cty.NumberIntVal(2), cty.NumberIntVal(2),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.UnknownVal(cty.Number), cty.UnknownVal(cty.Number),
true, `first argument must be a string, not number`,
}, },
{ {
cty.StringVal("1"), cty.StringVal("1"),
cty.NumberIntVal(63), cty.NumberIntVal(63),
cty.UnknownVal(cty.Number), cty.UnknownVal(cty.Number),
true, `base must be a whole number between 2 and 62 inclusive`,
}, },
{ {
cty.StringVal("1"), cty.StringVal("1"),
cty.NumberIntVal(-1), cty.NumberIntVal(-1),
cty.UnknownVal(cty.Number), cty.UnknownVal(cty.Number),
true, `base must be a whole number between 2 and 62 inclusive`,
}, },
{ {
cty.StringVal("1"), cty.StringVal("1"),
cty.NumberIntVal(1), cty.NumberIntVal(1),
cty.UnknownVal(cty.Number), cty.UnknownVal(cty.Number),
true, `base must be a whole number between 2 and 62 inclusive`,
}, },
{ {
cty.StringVal("1"), cty.StringVal("1"),
cty.NumberIntVal(0), cty.NumberIntVal(0),
cty.UnknownVal(cty.Number), cty.UnknownVal(cty.Number),
true, `base must be a whole number between 2 and 62 inclusive`,
}, },
{ {
cty.StringVal("1.2"), cty.StringVal("1.2"),
cty.NumberIntVal(10), cty.NumberIntVal(10),
cty.UnknownVal(cty.Number), 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) { t.Run(fmt.Sprintf("parseint(%#v, %#v)", test.Num, test.Base), func(t *testing.T) {
got, err := ParseInt(test.Num, test.Base) got, err := ParseInt(test.Num, test.Base)
if test.Err { if test.Err != "" {
if err == nil { if err == nil {
t.Fatal("succeeded; want error") 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 return
} else if err != nil { } else if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)

View File

@ -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)
}
}

View File

@ -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)
}
})
}
}

View File