Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement projection escaping #35

Merged
merged 1 commit into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/commands/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ func Test_Execute(t *testing.T) {
policies: []string{"../../testdata/tf-plan/policy.yaml"},
out: "../../testdata/tf-plan/out.txt",
wantErr: false,
}, {
name: "escaped",
payload: "../../testdata/escaped/payload.yaml",
policies: []string{"../../testdata/escaped/policy.yaml"},
out: "../../testdata/escaped/out.txt",
wantErr: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/engine/assert/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func NewContextBinding(path *field.Path, bindings binding.Bindings, value interf
func() (interface{}, error) {
expression := parseExpression(entry.Variable.Value)
if expression != nil && expression.engine != "" {
if expression.foreach != "" {
if expression.foreachName != "" {
return nil, field.Invalid(path.Child("variable", "value"), entry.Variable.Value, "foreach is not supported in context")
}
if expression.binding != "" {
Expand Down
89 changes: 43 additions & 46 deletions pkg/engine/assert/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,62 @@ package assert
import (
"reflect"
"regexp"
"strings"

reflectutils "github.com/kyverno/kyverno-json/pkg/utils/reflect"
)

const (
defaultForeachVariable = "index"
expressionPrefix = "("
expressionSuffix = ")"
legacyExpressionPrefix = "{{"
legacyExpressionSuffix = "}}"
)

var (
foreachRegex = regexp.MustCompile(`^~(?:(\w*)\.)?`)
bindingRegex = regexp.MustCompile(`@(\w*)$`)
foreachRegex = regexp.MustCompile(`^~(?:(\w+)\.)?(.*)`)
bindingRegex = regexp.MustCompile(`(.*)@(\w+)$`)
escapeRegex = regexp.MustCompile(`^/(.+)/$`)
engineRegex = regexp.MustCompile(`^\((?:(\w+):)?(.+)\)$`)
)

type expression struct {
foreach string
statement string
binding string
engine string
foreach bool
foreachName string
statement string
binding string
engine string
}

func parseExpression(value interface{}) *expression {
if reflectutils.GetKind(value) != reflect.String {
return nil
func parseExpressionRegex(in string) *expression {
expression := &expression{}
// 1. match foreach
if match := foreachRegex.FindStringSubmatch(in); match != nil {
expression.foreach = true
expression.foreachName = match[1]
in = match[2]
}
// 2. match binding
if match := bindingRegex.FindStringSubmatch(in); match != nil {
expression.binding = match[2]
in = match[1]
}
statement := reflect.ValueOf(value).String()
foreach := ""
binding := ""
engine := ""
if match := foreachRegex.FindStringSubmatch(statement); match != nil {
foreach = match[1]
if foreach == "" {
foreach = defaultForeachVariable
// 3. match escape, if there's no escaping then match engine
if match := escapeRegex.FindStringSubmatch(in); match != nil {
in = match[1]
} else {
if match := engineRegex.FindStringSubmatch(in); match != nil {
expression.engine = match[1]
// account for default engine
if expression.engine == "" {
expression.engine = "jp"
}
in = match[2]
}
statement = strings.TrimPrefix(statement, match[0])
}
if match := bindingRegex.FindStringSubmatch(statement); match != nil {
binding = match[1]
statement = strings.TrimSuffix(statement, match[0])
// parse statement
expression.statement = in
if expression.statement == "" {
return nil
}
if strings.HasPrefix(statement, legacyExpressionPrefix) {
statement = strings.TrimPrefix(statement, legacyExpressionPrefix)
statement = strings.TrimSuffix(statement, legacyExpressionSuffix)
engine = "jp"
} else if strings.HasPrefix(statement, expressionPrefix) {
statement = strings.TrimPrefix(statement, expressionPrefix)
statement = strings.TrimSuffix(statement, expressionSuffix)
engine = "jp"
} /* else if binding == "" {
binding = strings.TrimSpace(statement)
}*/
return &expression{
foreach: foreach,
statement: strings.TrimSpace(statement),
binding: binding,
engine: engine,
return expression
}

func parseExpression(value interface{}) *expression {
if reflectutils.GetKind(value) != reflect.String {
return nil
}
return parseExpressionRegex(reflect.ValueOf(value).String())
}
177 changes: 177 additions & 0 deletions pkg/engine/assert/expression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package assert

import (
"reflect"
"testing"
)

func Test_parseExpressionRegex(t *testing.T) {
tests := []struct {
name string
in string
want *expression
}{{
name: "empty",
in: "",
want: nil,
}, {
name: "simple field",
in: "test",
want: &expression{
foreach: false,
foreachName: "",
statement: "test",
},
}, {
name: "simple field",
in: "(test)",
want: &expression{
foreach: false,
foreachName: "",
statement: "test",
engine: "jp",
},
}, {
name: "nested field",
in: "test.test",
want: &expression{
foreach: false,
foreachName: "",
statement: "test.test",
},
}, {
name: "nested field",
in: "(test.test)",
want: &expression{
foreach: false,
foreachName: "",
statement: "test.test",
engine: "jp",
},
}, {
name: "foreach simple field",
in: "~test",
want: &expression{
foreach: true,
foreachName: "",
statement: "test",
},
}, {
name: "foreach simple field",
in: "~(test)",
want: &expression{
foreach: true,
foreachName: "",
statement: "test",
engine: "jp",
},
}, {
name: "foreach nested field",
in: "~(test.test)",
want: &expression{
foreach: true,
foreachName: "",
statement: "test.test",
engine: "jp",
},
}, {
name: "binding",
in: "test@foo",
want: &expression{
foreach: false,
foreachName: "",
statement: "test",
binding: "foo",
},
}, {
name: "binding",
in: "(test)@foo",
want: &expression{
foreach: false,
foreachName: "",
statement: "test",
binding: "foo",
engine: "jp",
},
}, {
name: "foreach and binding",
in: "~test@foo",
want: &expression{
foreach: true,
foreachName: "",
statement: "test",
binding: "foo",
},
}, {
name: "foreach and binding",
in: "~(test)@foo",
want: &expression{
foreach: true,
foreachName: "",
statement: "test",
binding: "foo",
engine: "jp",
},
}, {
name: "escape",
in: "/~(test)@foo/",
want: &expression{
foreach: false,
foreachName: "",
statement: "~(test)@foo",
binding: "",
},
}, {
name: "escape",
in: "/test/",
want: &expression{
foreach: false,
foreachName: "",
statement: "test",
binding: "",
},
}, {
name: "escape",
in: "/(test)/",
want: &expression{
foreach: false,
foreachName: "",
statement: "(test)",
binding: "",
},
}, {
name: "escape",
in: "//test//",
want: &expression{
foreach: false,
foreachName: "",
statement: "/test/",
binding: "",
},
}, {
name: "escape",
in: "~index./(test)/",
want: &expression{
foreach: true,
foreachName: "index",
statement: "(test)",
binding: "",
},
}, {
name: "escape",
in: "~index./(test)/@name",
want: &expression{
foreach: true,
foreachName: "index",
statement: "(test)",
binding: "name",
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseExpressionRegex(tt.in); !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseExpressionRegex() = %v, want %v", got, tt.want)
}
})
}
}
30 changes: 18 additions & 12 deletions pkg/engine/assert/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,41 +40,47 @@ type mapNode map[interface{}]Assertion
func (n mapNode) assert(path *field.Path, value interface{}, bindings jpbinding.Bindings) (field.ErrorList, error) {
var errs field.ErrorList
for k, v := range n {
projected, foreach, binding, err := project(k, value, bindings)
projection, err := project(k, value, bindings)
if err != nil {
return nil, field.InternalError(path.Child(fmt.Sprint(k)), err)
} else {
if binding != "" {
bindings = bindings.Register("$"+binding, jpbinding.NewBinding(projected))
if projection.binding != "" {
bindings = bindings.Register("$"+projection.binding, jpbinding.NewBinding(projection.result))
}
if foreach != "" {
projectedKind := reflectutils.GetKind(projected)
if projection.foreach {
projectedKind := reflectutils.GetKind(projection.result)
if projectedKind == reflect.Slice {
valueOf := reflect.ValueOf(projected)
valueOf := reflect.ValueOf(projection.result)
for i := 0; i < valueOf.Len(); i++ {
bindings := bindings.Register("$"+foreach, jpbinding.NewBinding(i))
bindings := bindings
if projection.foreachName != "" {
bindings = bindings.Register("$"+projection.foreachName, jpbinding.NewBinding(i))
}
if _errs, err := v.assert(path.Child(fmt.Sprint(k)).Index(i), valueOf.Index(i).Interface(), bindings); err != nil {
return nil, err
} else {
errs = append(errs, _errs...)
}
}
} else if projectedKind == reflect.Map {
iter := reflect.ValueOf(projected).MapRange()
iter := reflect.ValueOf(projection.result).MapRange()
for iter.Next() {
key := iter.Key().Interface()
bindings := bindings.Register("$"+foreach, jpbinding.NewBinding(key))
bindings := bindings
if projection.foreachName != "" {
bindings = bindings.Register("$"+projection.foreachName, jpbinding.NewBinding(key))
}
if _errs, err := v.assert(path.Child(fmt.Sprint(k)).Key(fmt.Sprint(key)), iter.Value().Interface(), bindings); err != nil {
return nil, err
} else {
errs = append(errs, _errs...)
}
}
} else {
return nil, field.TypeInvalid(path.Child(fmt.Sprint(k)), projected, "expected a slice or a map")
return nil, field.TypeInvalid(path.Child(fmt.Sprint(k)), projection.result, "expected a slice or a map")
}
} else {
if _errs, err := v.assert(path.Child(fmt.Sprint(k)), projected, bindings); err != nil {
if _errs, err := v.assert(path.Child(fmt.Sprint(k)), projection.result, bindings); err != nil {
return nil, err
} else {
errs = append(errs, _errs...)
Expand Down Expand Up @@ -125,7 +131,7 @@ func (n *scalarNode) assert(path *field.Path, value interface{}, bindings bindin
// this is to avoid the case where the value is a map and the RHS is a string
// TODO: we need a way to escape the projection
if expression != nil && expression.engine != "" {
if expression.foreach != "" {
if expression.foreachName != "" {
return nil, field.Invalid(path, rhs, "foreach is not supported on the RHS")
}
if expression.binding != "" {
Expand Down
Loading