apiserver/pkg/cel/environment/environment_test.go

327 lines
10 KiB
Go

/*
Copyright 2023 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 environment
import (
"context"
"fmt"
"testing"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
)
type envTypeAndVersion struct {
version *version.Version
envType Type
}
func TestBaseEnvironment(t *testing.T) {
widgetsType := apiservercel.NewObjectType("Widget",
map[string]*apiservercel.DeclField{
"x": {
Name: "x",
Type: apiservercel.StringType,
},
})
cases := []struct {
name string
typeVersionCombinations []envTypeAndVersion
validExpressions []string
invalidExpressions []string
activation any
opts []VersionedOptions
}{
{
name: "core settings enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 23), NewExpressions},
{version.MajorMinor(1, 23), StoredExpressions},
},
validExpressions: []string{
"[1, 2, 3].indexOf(2) == 1", // lists
"'abc'.contains('bc')", //strings
"isURL('http://example.com')", // urls
"'a 1 b 2'.find('[0-9]') == '1'", // regex
},
},
{
name: "authz disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 26), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"authorizer.path('/healthz').check('get').allowed()"},
activation: map[string]any{"authorizer": library.NewAuthorizerVal(nil, fakeAuthorizer{decision: authorizer.DecisionAllow})},
opts: []VersionedOptions{
{IntroducedVersion: version.MajorMinor(1, 27), EnvOptions: []cel.EnvOption{cel.Variable("authorizer", library.AuthorizerType)}},
},
},
{
name: "authz enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
{version.MajorMinor(1, 26), StoredExpressions},
},
validExpressions: []string{"authorizer.path('/healthz').check('get').allowed()"},
activation: map[string]any{"authorizer": library.NewAuthorizerVal(nil, fakeAuthorizer{decision: authorizer.DecisionAllow})},
opts: []VersionedOptions{
{IntroducedVersion: version.MajorMinor(1, 27), EnvOptions: []cel.EnvOption{cel.Variable("authorizer", library.AuthorizerType)}},
},
},
{
name: "cross numeric comparisons disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"1.5 > 1"},
},
{
name: "cross numeric comparisons enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
{version.MajorMinor(1, 27), StoredExpressions},
},
validExpressions: []string{"1.5 > 1"},
},
{
name: "user defined variable disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"fizz == 'buzz'"},
activation: map[string]any{"fizz": "buzz"},
opts: []VersionedOptions{
{IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{cel.Variable("fizz", cel.StringType)}},
},
},
{
name: "user defined variable enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
{version.MajorMinor(1, 27), StoredExpressions},
},
validExpressions: []string{"fizz == 'buzz'"},
activation: map[string]any{"fizz": "buzz"},
opts: []VersionedOptions{
{IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{cel.Variable("fizz", cel.StringType)}},
},
},
{
name: "declared type enabled before removed",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
// always disabled for StoredExpressions
},
validExpressions: []string{"widget.x == 'buzz'"},
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
RemovedVersion: version.MajorMinor(1, 29),
DeclTypes: []*apiservercel.DeclType{widgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("widget", cel.ObjectType("Widget")),
},
},
},
},
{
name: "declared type disabled after removed",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 29), NewExpressions},
{version.MajorMinor(1, 29), StoredExpressions},
},
invalidExpressions: []string{"widget.x == 'buzz'"},
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
RemovedVersion: version.MajorMinor(1, 29),
DeclTypes: []*apiservercel.DeclType{widgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("widget", cel.ObjectType("Widget")),
},
},
},
},
{
name: "declared type disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"widget.x == 'buzz'"},
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
DeclTypes: []*apiservercel.DeclType{widgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("widget", widgetsType.CelType()),
},
},
},
},
{
name: "declared type enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
{version.MajorMinor(1, 27), StoredExpressions},
},
validExpressions: []string{"widget.x == 'buzz'"},
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
DeclTypes: []*apiservercel.DeclType{widgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("widget", widgetsType.CelType()),
},
},
},
},
{
name: "library version 0 enabled, version 1 disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
// version 1 always enabled for StoredExpressions
},
validExpressions: []string{"test() == true"},
invalidExpressions: []string{"testV1() == true"},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 27),
RemovedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
library.Test(library.TestVersion(0)),
},
},
{
IntroducedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
library.Test(library.TestVersion(1)),
},
},
},
},
{
name: "library version 0 disabled, version 1 enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
{version.MajorMinor(1, 26), StoredExpressions},
{version.MajorMinor(1, 27), StoredExpressions},
{version.MajorMinor(1, 28), StoredExpressions},
},
validExpressions: []string{"test() == false", "testV1() == true"},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 27),
RemovedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
library.Test(library.TestVersion(0)),
},
},
{
IntroducedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
library.Test(library.TestVersion(1)),
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
activation := tc.activation
if activation == nil {
activation = map[string]any{}
}
for _, tv := range tc.typeVersionCombinations {
t.Run(fmt.Sprintf("version=%s,envType=%s", tv.version.String(), tv.envType), func(t *testing.T) {
envSet := MustBaseEnvSet(tv.version)
if tc.opts != nil {
var err error
envSet, err = envSet.Extend(tc.opts...)
if err != nil {
t.Errorf("unexpected error extending environment %v", err)
}
}
envType := NewExpressions
if len(tv.envType) > 0 {
envType = tv.envType
}
validationEnv, err := envSet.Env(envType)
if err != nil {
t.Fatal(err)
}
for _, valid := range tc.validExpressions {
if ok, err := isValid(validationEnv, valid, activation); !ok {
if err != nil {
t.Errorf("expected expression to be valid but got %v", err)
}
t.Error("expected expression to return true")
}
}
for _, invalid := range tc.invalidExpressions {
if ok, _ := isValid(validationEnv, invalid, activation); ok {
t.Errorf("expected invalid expression to result in error")
}
}
})
}
})
}
}
func isValid(env *cel.Env, expr string, activation any) (bool, error) {
ast, issues := env.Compile(expr)
if len(issues.Errors()) > 0 {
return false, issues.Err()
}
prog, err := env.Program(ast)
if err != nil {
return false, err
}
result, _, err := prog.Eval(activation)
if err != nil {
return false, err
}
return result.Value() == true, nil
}
type fakeAuthorizer struct {
decision authorizer.Decision
reason string
err error
}
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
return f.decision, f.reason, f.err
}