k8s.io/apiserver@v0.31.1/pkg/cel/environment/environment_test.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package environment
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  
    24  	"github.com/google/cel-go/cel"
    25  
    26  	"k8s.io/apimachinery/pkg/util/version"
    27  	"k8s.io/apiserver/pkg/authorization/authorizer"
    28  	apiservercel "k8s.io/apiserver/pkg/cel"
    29  	"k8s.io/apiserver/pkg/cel/library"
    30  )
    31  
    32  type envTypeAndVersion struct {
    33  	version *version.Version
    34  	envType Type
    35  }
    36  
    37  func TestBaseEnvironment(t *testing.T) {
    38  	widgetsType := apiservercel.NewObjectType("Widget",
    39  		map[string]*apiservercel.DeclField{
    40  			"x": {
    41  				Name: "x",
    42  				Type: apiservercel.StringType,
    43  			},
    44  		})
    45  
    46  	// The escaping happens while construct declType hence we use escaped format here directly.
    47  	gadgetsType := apiservercel.NewObjectType("Gadget",
    48  		map[string]*apiservercel.DeclField{
    49  			"__namespace__": {
    50  				Name: "__namespace__",
    51  				Type: apiservercel.StringType,
    52  			},
    53  		})
    54  
    55  	cases := []struct {
    56  		name                    string
    57  		typeVersionCombinations []envTypeAndVersion
    58  		validExpressions        []string
    59  		invalidExpressions      []string
    60  		activation              any
    61  		opts                    []VersionedOptions
    62  	}{
    63  		{
    64  			name: "core settings enabled",
    65  			typeVersionCombinations: []envTypeAndVersion{
    66  				{version.MajorMinor(1, 23), NewExpressions},
    67  				{version.MajorMinor(1, 23), StoredExpressions},
    68  			},
    69  			validExpressions: []string{
    70  				"[1, 2, 3].indexOf(2) == 1",      // lists
    71  				"'abc'.contains('bc')",           //strings
    72  				"isURL('http://example.com')",    // urls
    73  				"'a 1 b 2'.find('[0-9]') == '1'", // regex
    74  			},
    75  		},
    76  		{
    77  			name: "authz disabled",
    78  			typeVersionCombinations: []envTypeAndVersion{
    79  				{version.MajorMinor(1, 26), NewExpressions},
    80  				// always enabled for StoredExpressions
    81  			},
    82  			invalidExpressions: []string{"authorizer.path('/healthz').check('get').allowed()"},
    83  			activation:         map[string]any{"authorizer": library.NewAuthorizerVal(nil, fakeAuthorizer{decision: authorizer.DecisionAllow})},
    84  			opts: []VersionedOptions{
    85  				{IntroducedVersion: version.MajorMinor(1, 27), EnvOptions: []cel.EnvOption{cel.Variable("authorizer", library.AuthorizerType)}},
    86  			},
    87  		},
    88  		{
    89  			name: "authz enabled",
    90  			typeVersionCombinations: []envTypeAndVersion{
    91  				{version.MajorMinor(1, 27), NewExpressions},
    92  				{version.MajorMinor(1, 26), StoredExpressions},
    93  			},
    94  			validExpressions: []string{"authorizer.path('/healthz').check('get').allowed()"},
    95  			activation:       map[string]any{"authorizer": library.NewAuthorizerVal(nil, fakeAuthorizer{decision: authorizer.DecisionAllow})},
    96  			opts: []VersionedOptions{
    97  				{IntroducedVersion: version.MajorMinor(1, 27), EnvOptions: []cel.EnvOption{cel.Variable("authorizer", library.AuthorizerType)}},
    98  			},
    99  		},
   100  		{
   101  			name: "cross numeric comparisons disabled",
   102  			typeVersionCombinations: []envTypeAndVersion{
   103  				{version.MajorMinor(1, 27), NewExpressions},
   104  				// always enabled for StoredExpressions
   105  			},
   106  			invalidExpressions: []string{"1.5 > 1"},
   107  		},
   108  		{
   109  			name: "cross numeric comparisons enabled",
   110  			typeVersionCombinations: []envTypeAndVersion{
   111  				{version.MajorMinor(1, 28), NewExpressions},
   112  				{version.MajorMinor(1, 27), StoredExpressions},
   113  			},
   114  			validExpressions: []string{"1.5 > 1"},
   115  		},
   116  		{
   117  			name: "user defined variable disabled",
   118  			typeVersionCombinations: []envTypeAndVersion{
   119  				{version.MajorMinor(1, 27), NewExpressions},
   120  				// always enabled for StoredExpressions
   121  			},
   122  			invalidExpressions: []string{"fizz == 'buzz'"},
   123  			activation:         map[string]any{"fizz": "buzz"},
   124  			opts: []VersionedOptions{
   125  				{IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{cel.Variable("fizz", cel.StringType)}},
   126  			},
   127  		},
   128  		{
   129  			name: "user defined variable enabled",
   130  			typeVersionCombinations: []envTypeAndVersion{
   131  				{version.MajorMinor(1, 28), NewExpressions},
   132  				{version.MajorMinor(1, 27), StoredExpressions},
   133  			},
   134  			validExpressions: []string{"fizz == 'buzz'"},
   135  			activation:       map[string]any{"fizz": "buzz"},
   136  			opts: []VersionedOptions{
   137  				{IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{cel.Variable("fizz", cel.StringType)}},
   138  			},
   139  		},
   140  		{
   141  			name: "declared type enabled before removed",
   142  			typeVersionCombinations: []envTypeAndVersion{
   143  				{version.MajorMinor(1, 28), NewExpressions},
   144  				// always disabled for StoredExpressions
   145  			},
   146  			validExpressions: []string{"widget.x == 'buzz'"},
   147  			activation:       map[string]any{"widget": map[string]any{"x": "buzz"}},
   148  			opts: []VersionedOptions{
   149  				{
   150  					IntroducedVersion: version.MajorMinor(1, 28),
   151  					RemovedVersion:    version.MajorMinor(1, 29),
   152  					DeclTypes:         []*apiservercel.DeclType{widgetsType},
   153  					EnvOptions: []cel.EnvOption{
   154  						cel.Variable("widget", cel.ObjectType("Widget")),
   155  					},
   156  				},
   157  			},
   158  		},
   159  		{
   160  			name: "declared type disabled after removed",
   161  			typeVersionCombinations: []envTypeAndVersion{
   162  				{version.MajorMinor(1, 29), NewExpressions},
   163  				{version.MajorMinor(1, 29), StoredExpressions},
   164  			},
   165  			invalidExpressions: []string{"widget.x == 'buzz'"},
   166  			activation:         map[string]any{"widget": map[string]any{"x": "buzz"}},
   167  			opts: []VersionedOptions{
   168  				{
   169  					IntroducedVersion: version.MajorMinor(1, 28),
   170  					RemovedVersion:    version.MajorMinor(1, 29),
   171  					DeclTypes:         []*apiservercel.DeclType{widgetsType},
   172  					EnvOptions: []cel.EnvOption{
   173  						cel.Variable("widget", cel.ObjectType("Widget")),
   174  					},
   175  				},
   176  			},
   177  		},
   178  		{
   179  			name: "declared type disabled",
   180  			typeVersionCombinations: []envTypeAndVersion{
   181  				{version.MajorMinor(1, 27), NewExpressions},
   182  				// always enabled for StoredExpressions
   183  			},
   184  			invalidExpressions: []string{"widget.x == 'buzz'"},
   185  			activation:         map[string]any{"widget": map[string]any{"x": "buzz"}},
   186  			opts: []VersionedOptions{
   187  				{
   188  					IntroducedVersion: version.MajorMinor(1, 28),
   189  					DeclTypes:         []*apiservercel.DeclType{widgetsType},
   190  					EnvOptions: []cel.EnvOption{
   191  						cel.Variable("widget", widgetsType.CelType()),
   192  					},
   193  				},
   194  			},
   195  		},
   196  		{
   197  			name: "declared type enabled",
   198  			typeVersionCombinations: []envTypeAndVersion{
   199  				{version.MajorMinor(1, 28), NewExpressions},
   200  				{version.MajorMinor(1, 27), StoredExpressions},
   201  			},
   202  			validExpressions: []string{"widget.x == 'buzz'"},
   203  			activation:       map[string]any{"widget": map[string]any{"x": "buzz"}},
   204  			opts: []VersionedOptions{
   205  				{
   206  					IntroducedVersion: version.MajorMinor(1, 28),
   207  					DeclTypes:         []*apiservercel.DeclType{widgetsType},
   208  					EnvOptions: []cel.EnvOption{
   209  						cel.Variable("widget", widgetsType.CelType()),
   210  					},
   211  				},
   212  			},
   213  		},
   214  		{
   215  			name: "library version 0 enabled, version 1 disabled",
   216  			typeVersionCombinations: []envTypeAndVersion{
   217  				{version.MajorMinor(1, 27), NewExpressions},
   218  				// version 1 always enabled for StoredExpressions
   219  			},
   220  			validExpressions:   []string{"test() == true"},
   221  			invalidExpressions: []string{"testV1() == true"},
   222  			opts: []VersionedOptions{
   223  				{
   224  					IntroducedVersion: version.MajorMinor(1, 27),
   225  					RemovedVersion:    version.MajorMinor(1, 28),
   226  					EnvOptions: []cel.EnvOption{
   227  						library.Test(library.TestVersion(0)),
   228  					},
   229  				},
   230  				{
   231  					IntroducedVersion: version.MajorMinor(1, 28),
   232  					EnvOptions: []cel.EnvOption{
   233  						library.Test(library.TestVersion(1)),
   234  					},
   235  				},
   236  			},
   237  		},
   238  		{
   239  			name: "library version 0 disabled, version 1 enabled",
   240  			typeVersionCombinations: []envTypeAndVersion{
   241  				{version.MajorMinor(1, 28), NewExpressions},
   242  				{version.MajorMinor(1, 26), StoredExpressions},
   243  				{version.MajorMinor(1, 27), StoredExpressions},
   244  				{version.MajorMinor(1, 28), StoredExpressions},
   245  			},
   246  			validExpressions: []string{"test() == false", "testV1() == true"},
   247  			opts: []VersionedOptions{
   248  				{
   249  					IntroducedVersion: version.MajorMinor(1, 27),
   250  					RemovedVersion:    version.MajorMinor(1, 28),
   251  					EnvOptions: []cel.EnvOption{
   252  						library.Test(library.TestVersion(0)),
   253  					},
   254  				},
   255  				{
   256  					IntroducedVersion: version.MajorMinor(1, 28),
   257  					EnvOptions: []cel.EnvOption{
   258  						library.Test(library.TestVersion(1)),
   259  					},
   260  				},
   261  			},
   262  		},
   263  		{
   264  			name: "recognizeKeywordAsFieldName disabled",
   265  			typeVersionCombinations: []envTypeAndVersion{
   266  				{version.MajorMinor(1, 30), NewExpressions},
   267  				// always enabled for StoredExpressions
   268  			},
   269  			invalidExpressions: []string{"gadget.namespace == 'buzz'"},
   270  			activation:         map[string]any{"gadget": map[string]any{"namespace": "buzz"}},
   271  			opts: []VersionedOptions{
   272  				{
   273  					IntroducedVersion: version.MajorMinor(1, 28),
   274  					DeclTypes:         []*apiservercel.DeclType{gadgetsType},
   275  					EnvOptions: []cel.EnvOption{
   276  						cel.Variable("gadget", cel.ObjectType("Gadget")),
   277  					},
   278  				},
   279  			},
   280  		},
   281  		{
   282  			name: "recognizeKeywordAsFieldName enabled",
   283  			typeVersionCombinations: []envTypeAndVersion{
   284  				{version.MajorMinor(1, 31), NewExpressions},
   285  				{version.MajorMinor(1, 30), StoredExpressions},
   286  			},
   287  			validExpressions: []string{"gadget.namespace == 'buzz'"},
   288  			activation:       map[string]any{"gadget": map[string]any{"namespace": "buzz"}},
   289  			opts: []VersionedOptions{
   290  				{
   291  					IntroducedVersion: version.MajorMinor(1, 28),
   292  					DeclTypes:         []*apiservercel.DeclType{gadgetsType},
   293  					EnvOptions: []cel.EnvOption{
   294  						cel.Variable("gadget", cel.ObjectType("Gadget")),
   295  					},
   296  				},
   297  			},
   298  		},
   299  	}
   300  
   301  	for _, tc := range cases {
   302  		t.Run(tc.name, func(t *testing.T) {
   303  			activation := tc.activation
   304  			if activation == nil {
   305  				activation = map[string]any{}
   306  			}
   307  			for _, tv := range tc.typeVersionCombinations {
   308  				t.Run(fmt.Sprintf("version=%s,envType=%s", tv.version.String(), tv.envType), func(t *testing.T) {
   309  
   310  					envSet := MustBaseEnvSet(tv.version, true)
   311  					if tc.opts != nil {
   312  						var err error
   313  						envSet, err = envSet.Extend(tc.opts...)
   314  						if err != nil {
   315  							t.Errorf("unexpected error extending environment %v", err)
   316  						}
   317  					}
   318  
   319  					envType := NewExpressions
   320  					if len(tv.envType) > 0 {
   321  						envType = tv.envType
   322  					}
   323  
   324  					validationEnv, err := envSet.Env(envType)
   325  					if err != nil {
   326  						t.Fatal(err)
   327  					}
   328  					for _, valid := range tc.validExpressions {
   329  						if ok, err := isValid(validationEnv, valid, activation); !ok {
   330  							if err != nil {
   331  								t.Errorf("expected expression to be valid but got %v", err)
   332  							}
   333  							t.Error("expected expression to return true")
   334  						}
   335  					}
   336  					for _, invalid := range tc.invalidExpressions {
   337  						if ok, _ := isValid(validationEnv, invalid, activation); ok {
   338  							t.Errorf("expected invalid expression to result in error")
   339  						}
   340  					}
   341  				})
   342  			}
   343  		})
   344  	}
   345  }
   346  
   347  func isValid(env *cel.Env, expr string, activation any) (bool, error) {
   348  	ast, issues := env.Compile(expr)
   349  	if len(issues.Errors()) > 0 {
   350  		return false, issues.Err()
   351  	}
   352  	prog, err := env.Program(ast)
   353  	if err != nil {
   354  		return false, err
   355  	}
   356  	result, _, err := prog.Eval(activation)
   357  	if err != nil {
   358  		return false, err
   359  	}
   360  	return result.Value() == true, nil
   361  }
   362  
   363  type fakeAuthorizer struct {
   364  	decision authorizer.Decision
   365  	reason   string
   366  	err      error
   367  }
   368  
   369  func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
   370  	return f.decision, f.reason, f.err
   371  }