Skip to content

Commit a586c26

Browse files
dhaiducekopenshift-merge-bot[bot]
authored andcommitted
Expand context kinds
With the `.Object` variable, Kubernetes objects could have a variety of kinds. Expand the context check to allow these kinds, avoiding pointers, channels, and functions. ref: https://issues.redhat.com/browse/ACM-20863 Signed-off-by: Dale Haiducek <19750917+dhaiducek@users.noreply.github.com>
1 parent 354f967 commit a586c26

File tree

2 files changed

+95
-38
lines changed

2 files changed

+95
-38
lines changed

pkg/templates/templates.go

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ var (
5050
ErrProtectNotEnabled = errors.New("the protect template function is not enabled in this mode")
5151
ErrNewLinesNotAllowed = errors.New("new lines are not allowed in the string passed to the toLiteral function")
5252
ErrInvalidContextType = errors.New(
53-
"the input context must be a struct with fields (recursively) of type string, map[string]string, " +
54-
"map[string]interface{}, or struct",
53+
"the input context must be a struct that recurses to kinds bool, int, float, or string",
5554
)
5655
ErrMissingNamespace = errors.New(
5756
"the lookup of a single namespaced resource must have a namespace specified",
@@ -385,10 +384,10 @@ func getValidContext(value interface{}) (interface{}, error) {
385384

386385
// Require the context to be a struct
387386
if reflect.TypeOf(value).Kind() != reflect.Struct {
388-
return nil, fmt.Errorf("%w, got %s", ErrInvalidContextType, reflect.TypeOf(value))
387+
return nil, fmt.Errorf("%w, but found a parent of kind %s", ErrInvalidContextType, reflect.TypeOf(value))
389388
}
390389

391-
// Require the context to have fields of strings or maps/structs with string/map values/fields.
390+
// Require the context to recurse to primitive keys and values through structs, maps, or arrays
392391
err := getValidContextHelper(value)
393392
if err != nil {
394393
return nil, err
@@ -397,49 +396,85 @@ func getValidContext(value interface{}) (interface{}, error) {
397396
return value, nil
398397
}
399398

400-
func getValidContextHelper(value interface{}) error {
399+
// isPrimitive detects primitive types from the reflect package
400+
func isPrimitive(kind reflect.Kind) bool {
401+
switch kind {
402+
case
403+
reflect.Bool,
404+
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
405+
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
406+
reflect.Float32, reflect.Float64,
407+
reflect.String:
408+
return true
409+
410+
default:
411+
return false
412+
}
413+
}
414+
415+
func getValidContextHelper(value any) error {
401416
f := reflect.TypeOf(value)
402417

403-
switch f.Kind() {
404-
case reflect.String:
418+
// Allow primitive types (excludes complex numbers)
419+
if isPrimitive(f.Kind()) {
405420
return nil
421+
}
422+
423+
// Handle complex types
424+
switch f.Kind() {
425+
// Allow arrays and recurse into each item
426+
case reflect.Slice, reflect.Array:
427+
// Iterate over embedded maps and interfaces
428+
if f.Elem().Kind() == reflect.Interface || f.Elem().Kind() == reflect.Map {
429+
for i := range reflect.ValueOf(value).Len() {
430+
err := getValidContextHelper(reflect.ValueOf(value).Index(i).Interface())
431+
if err != nil {
432+
return err
433+
}
434+
}
435+
} else if !isPrimitive(f.Elem().Kind()) {
436+
// Verify map values are primitive
437+
return fmt.Errorf("%w, found an array with values of kind %s", ErrInvalidContextType, reflect.TypeOf(value))
438+
}
439+
440+
// Allow structs and recurse into fields
406441
case reflect.Struct:
407442
for i := range f.NumField() {
408-
err := getValidContextHelper(reflect.ValueOf(value).Field(i).Interface())
409-
if err != nil {
410-
return err
443+
// Only handle exported struct fields (causes a panic calling Interface())
444+
if f.Field(i).IsExported() {
445+
err := getValidContextHelper(reflect.ValueOf(value).Field(i).Interface())
446+
if err != nil {
447+
return err
448+
}
411449
}
412450
}
413451

414-
return nil
452+
// Allow maps and recurse into keys
415453
case reflect.Map:
416-
// Only allow string keys in maps
417-
if f.Key().Kind() != reflect.String {
418-
return ErrInvalidContextType
454+
// Only allow primitive keys in maps
455+
if !isPrimitive(f.Key().Kind()) {
456+
return fmt.Errorf("%w, found a map with keys of kind %s", ErrInvalidContextType, f.Key().Kind())
419457
}
420458

421-
// Check if it's a map of maps/strings. This is to allow things such as the Kubernetes metadata field which has
422-
// string (e.g. name) and map[string]string (e.g. labels) values.
459+
// Iterate over embedded maps and interfaces
423460
if f.Elem().Kind() == reflect.Interface || f.Elem().Kind() == reflect.Map {
424461
for _, key := range reflect.ValueOf(value).MapKeys() {
425462
err := getValidContextHelper(reflect.ValueOf(value).MapIndex(key).Interface())
426463
if err != nil {
427464
return err
428465
}
429466
}
430-
431-
return nil
432-
}
433-
434-
// Check if it's map[string]string
435-
if f.Elem().Kind() != reflect.String {
436-
return ErrInvalidContextType
467+
} else if !isPrimitive(f.Elem().Kind()) {
468+
// Verify map values are primitive
469+
return fmt.Errorf("%w, found a map with values of kind %s", ErrInvalidContextType, reflect.TypeOf(value))
437470
}
438471

439-
return nil
472+
// Disallow all else like pointers and channels
440473
default:
441-
return ErrInvalidContextType
474+
return fmt.Errorf("%w, found value of kind %s", ErrInvalidContextType, reflect.TypeOf(value))
442475
}
476+
477+
return nil
443478
}
444479

445480
// validateEncryptionConfig validates an EncryptionConfig struct to ensure that if encryption

pkg/templates/templates_test.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,8 @@ func TestResolveTemplateDefaultConfig(t *testing.T) {
737737
func TestResolveTemplateErrors(t *testing.T) {
738738
t.Parallel()
739739

740+
str := "world"
741+
740742
testcases := map[string]resolveTestCase{
741743
"toLiteral_with_newlines": {
742744
inputTmpl: `param: '{{ "something\n with\n new\n lines\n" | toLiteral }}'`,
@@ -754,29 +756,24 @@ func TestResolveTemplateErrors(t *testing.T) {
754756
ctx: 123,
755757
expectedErr: ErrInvalidContextType,
756758
},
757-
"invalid_context_nested_int": {
758-
inputTmpl: `test: '{{ printf "hello %s" "world" }}'`,
759-
ctx: struct{ ClusterID int }{12},
760-
expectedErr: ErrInvalidContextType,
761-
},
762-
"invalid_context_map_with_int": {
759+
"invalid_context_not_struct": {
763760
inputTmpl: `test: '{{ printf "hello %s" "world" }}'`,
764-
ctx: struct{ Foo map[string]int }{Foo: map[string]int{"bar": 12}},
761+
ctx: map[string]string{"hello": "world"},
765762
expectedErr: ErrInvalidContextType,
766763
},
767-
"invalid_context_map_of_int": {
764+
"invalid_context_pointer": {
768765
inputTmpl: `test: '{{ printf "hello %s" "world" }}'`,
769-
ctx: struct{ Foo map[int]string }{Foo: map[int]string{47: "something"}},
766+
ctx: struct{ Hello *string }{Hello: &str},
770767
expectedErr: ErrInvalidContextType,
771768
},
772-
"invalid_context_nested_struct": {
769+
"invalid_context_array_of_pointers": {
773770
inputTmpl: `test: '{{ printf "hello %s" "world" }}'`,
774-
ctx: struct{ Metadata struct{ NestedInt int } }{struct{ NestedInt int }{NestedInt: 3}},
771+
ctx: struct{ Hello []*string }{Hello: []*string{&str}},
775772
expectedErr: ErrInvalidContextType,
776773
},
777-
"invalid_context_not_struct": {
774+
"invalid_context_array_of_maps_to_pointers": {
778775
inputTmpl: `test: '{{ printf "hello %s" "world" }}'`,
779-
ctx: map[string]string{"hello": "world"},
776+
ctx: struct{ Hello []map[bool]*string }{Hello: []map[bool]*string{{false: &str}}},
780777
expectedErr: ErrInvalidContextType,
781778
},
782779
"disabled_fromSecret": {
@@ -911,11 +908,36 @@ func TestResolveTemplateWithContext(t *testing.T) {
911908
expectedResult: "test: SSBhbSBhIHJlYWxseSBsb25nIHRlbXBsYXRlIGZvciBjbHVzdGVyIGNsdXN0ZXIxIHRoYXQgbmVlZH" +
912909
"MgdG8gYmUgb3ZlciA4MCBjaGFyYWN0ZXJzIHRvIHRlc3Qgc29tZXRoaW5n",
913910
},
911+
"nested_int": {
912+
inputTmpl: `test: '{{ printf "hello %d" .ClusterID }}'`,
913+
ctx: struct{ ClusterID int }{12},
914+
expectedResult: "test: hello 12",
915+
},
916+
"nested_array": {
917+
inputTmpl: `value: '{{ index .Foo 0 }}'`,
918+
ctx: struct{ Foo []string }{Foo: []string{"hello"}},
919+
expectedResult: "value: hello",
920+
},
921+
"nested_map_with_int": {
922+
inputTmpl: `test: '{{ printf "hello %d" .Foo.bar }}'`,
923+
ctx: struct{ Foo map[string]int }{Foo: map[string]int{"bar": 12}},
924+
expectedResult: "test: hello 12",
925+
},
926+
"nested_map_of_int": {
927+
inputTmpl: `test: '{{ printf "hello %s" (index .Foo 47) }}'`,
928+
ctx: struct{ Foo map[int]string }{Foo: map[int]string{47: "something"}},
929+
expectedResult: "test: hello something",
930+
},
914931
"nested_map": {
915932
inputTmpl: `value: '{{ .Foo.greeting }}'`,
916933
ctx: struct{ Foo map[string]string }{Foo: map[string]string{"greeting": "hello"}},
917934
expectedResult: "value: hello",
918935
},
936+
"nested_struct_with_int": {
937+
inputTmpl: `test: '{{ printf "hello %d" .Metadata.NestedInt }}'`,
938+
ctx: struct{ Metadata struct{ NestedInt int } }{struct{ NestedInt int }{NestedInt: 3}},
939+
expectedResult: "test: hello 3",
940+
},
919941
"nested_struct": {
920942
inputTmpl: `value: '{{ .Metadata.Labels.hello }} {{ .Metadata.Namespace }}'`,
921943
ctx: struct{ Metadata partialMetadata }{

0 commit comments

Comments
 (0)