apiserver/pkg/cel/escaping_test.go

207 lines
6.9 KiB
Go

/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"fmt"
"regexp"
"testing"
fuzz "github.com/google/gofuzz"
)
// TestEscaping tests that property names are escaped as expected.
func TestEscaping(t *testing.T) {
cases := []struct {
unescaped string
escaped string
unescapable bool
}{
// '.', '-', '/' and '__' are escaped since
// CEL only allows identifiers of the form: [a-zA-Z_][a-zA-Z0-9_]*
{unescaped: "a.a", escaped: "a__dot__a"},
{unescaped: "a-a", escaped: "a__dash__a"},
{unescaped: "a__a", escaped: "a__underscores__a"},
{unescaped: "a.-/__a", escaped: "a__dot____dash____slash____underscores__a"},
{unescaped: "a._a", escaped: "a__dot___a"},
{unescaped: "a__.__a", escaped: "a__underscores____dot____underscores__a"},
{unescaped: "a___a", escaped: "a__underscores___a"},
{unescaped: "a____a", escaped: "a__underscores____underscores__a"},
{unescaped: "a__dot__a", escaped: "a__underscores__dot__underscores__a"},
{unescaped: "a__underscores__a", escaped: "a__underscores__underscores__underscores__a"},
// CEL lexer RESERVED keywords must be escaped
{unescaped: "true", escaped: "__true__"},
{unescaped: "false", escaped: "__false__"},
{unescaped: "null", escaped: "__null__"},
{unescaped: "in", escaped: "__in__"},
{unescaped: "as", escaped: "__as__"},
{unescaped: "break", escaped: "__break__"},
{unescaped: "const", escaped: "__const__"},
{unescaped: "continue", escaped: "__continue__"},
{unescaped: "else", escaped: "__else__"},
{unescaped: "for", escaped: "__for__"},
{unescaped: "function", escaped: "__function__"},
{unescaped: "if", escaped: "__if__"},
{unescaped: "import", escaped: "__import__"},
{unescaped: "let", escaped: "__let__"},
{unescaped: "loop", escaped: "__loop__"},
{unescaped: "package", escaped: "__package__"},
{unescaped: "namespace", escaped: "__namespace__"},
{unescaped: "return", escaped: "__return__"},
{unescaped: "var", escaped: "__var__"},
{unescaped: "void", escaped: "__void__"},
{unescaped: "while", escaped: "__while__"},
// Not all property names are escapable
{unescaped: "@", unescapable: true},
{unescaped: "1up", unescapable: true},
{unescaped: "👑", unescapable: true},
// CEL macro and function names do not need to be escaped because the parser keeps identifiers in a
// different namespace than function and macro names.
{unescaped: "has", escaped: "has"},
{unescaped: "all", escaped: "all"},
{unescaped: "exists", escaped: "exists"},
{unescaped: "exists_one", escaped: "exists_one"},
{unescaped: "filter", escaped: "filter"},
{unescaped: "size", escaped: "size"},
{unescaped: "contains", escaped: "contains"},
{unescaped: "startsWith", escaped: "startsWith"},
{unescaped: "endsWith", escaped: "endsWith"},
{unescaped: "matches", escaped: "matches"},
{unescaped: "duration", escaped: "duration"},
{unescaped: "timestamp", escaped: "timestamp"},
{unescaped: "getDate", escaped: "getDate"},
{unescaped: "getDayOfMonth", escaped: "getDayOfMonth"},
{unescaped: "getDayOfWeek", escaped: "getDayOfWeek"},
{unescaped: "getFullYear", escaped: "getFullYear"},
{unescaped: "getHours", escaped: "getHours"},
{unescaped: "getMilliseconds", escaped: "getMilliseconds"},
{unescaped: "getMinutes", escaped: "getMinutes"},
{unescaped: "getMonth", escaped: "getMonth"},
{unescaped: "getSeconds", escaped: "getSeconds"},
// we don't escape a single _
{unescaped: "_if", escaped: "_if"},
{unescaped: "_has", escaped: "_has"},
{unescaped: "_int", escaped: "_int"},
{unescaped: "_anything", escaped: "_anything"},
}
for _, tc := range cases {
t.Run(tc.unescaped, func(t *testing.T) {
e, escapable := Escape(tc.unescaped)
if tc.unescapable {
if escapable {
t.Errorf("Expected escapable=false, but got %t", escapable)
}
return
}
if !escapable {
t.Fatalf("Expected escapable=true, but got %t", escapable)
}
if tc.escaped != e {
t.Errorf("Expected %s to escape to %s, but got %s", tc.unescaped, tc.escaped, e)
}
if !validCelIdent.MatchString(e) {
t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", tc.unescaped, e)
}
u, ok := Unescape(tc.escaped)
if !ok {
t.Fatalf("Expected %s to be escapable, but it was not", tc.escaped)
}
if tc.unescaped != u {
t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, u)
}
})
}
}
func TestUnescapeMalformed(t *testing.T) {
for _, s := range []string{"__int__extra", "__illegal__"} {
t.Run(s, func(t *testing.T) {
e, ok := Unescape(s)
if ok {
t.Fatalf("Expected %s to be unescapable, but it escaped to: %s", s, e)
}
})
}
}
func TestEscapingFuzz(t *testing.T) {
fuzzer := fuzz.New()
for i := 0; i < 1000; i++ {
var unescaped string
fuzzer.Fuzz(&unescaped)
t.Run(fmt.Sprintf("%d - '%s'", i, unescaped), func(t *testing.T) {
if len(unescaped) == 0 {
return
}
escaped, ok := Escape(unescaped)
if !ok {
return
}
if !validCelIdent.MatchString(escaped) {
t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", unescaped, escaped)
}
u, ok := Unescape(escaped)
if !ok {
t.Fatalf("Expected %s to be unescapable, but it was not", escaped)
}
if unescaped != u {
t.Errorf("Expected %s to unescape to %s, but got %s", escaped, unescaped, u)
}
})
}
}
var validCelIdent = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
func TestCanSkipRegex(t *testing.T) {
cases := []struct {
unescaped string
canSkip bool
invalidCharFound bool
}{
{unescaped: "a.a", canSkip: false},
{unescaped: "a-a", canSkip: false},
{unescaped: "a__a", canSkip: false},
{unescaped: "a.-/__a", canSkip: false},
{unescaped: "a_a", canSkip: true},
{unescaped: "a_a_a", canSkip: true},
{unescaped: "@", invalidCharFound: true},
{unescaped: "👑", invalidCharFound: true},
// if escape keyword is detected before invalid character, invalidCharFound
{unescaped: "/👑", canSkip: false},
}
for _, tc := range cases {
t.Run(tc.unescaped, func(t *testing.T) {
escapeCheck := skipRegexCheck(tc.unescaped)
if escapeCheck.invalidCharFound {
if escapeCheck.invalidCharFound != tc.invalidCharFound {
t.Errorf("Expected input validation to be %v, but got %t", tc.invalidCharFound, escapeCheck.invalidCharFound)
}
} else {
if escapeCheck.canSkipRegex != tc.canSkip {
t.Errorf("Expected %v, but got %t", tc.canSkip, escapeCheck.canSkipRegex)
}
}
})
}
}