k8s.io/apiserver@v0.31.1/pkg/admission/plugin/cel/composition_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 cel
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/google/cel-go/cel"
    25  
    26  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apiserver/pkg/admission"
    28  	celconfig "k8s.io/apiserver/pkg/apis/cel"
    29  	"k8s.io/apiserver/pkg/cel/environment"
    30  )
    31  
    32  type testVariable struct {
    33  	name       string
    34  	expression string
    35  }
    36  
    37  func (t *testVariable) GetExpression() string {
    38  	return t.expression
    39  }
    40  
    41  func (t *testVariable) ReturnTypes() []*cel.Type {
    42  	return []*cel.Type{cel.AnyType}
    43  }
    44  
    45  func (t *testVariable) GetName() string {
    46  	return t.name
    47  }
    48  
    49  func TestCompositedPolicies(t *testing.T) {
    50  	cases := []struct {
    51  		name                  string
    52  		variables             []NamedExpressionAccessor
    53  		expression            string
    54  		attributes            admission.Attributes
    55  		expectedResult        any
    56  		expectErr             bool
    57  		expectedErrorMessage  string
    58  		runtimeCostBudget     int64
    59  		strictCostEnforcement bool
    60  	}{
    61  		{
    62  			name: "simple",
    63  			variables: []NamedExpressionAccessor{
    64  				&testVariable{
    65  					name:       "name",
    66  					expression: "object.metadata.name",
    67  				},
    68  			},
    69  			attributes:     endpointCreateAttributes(),
    70  			expression:     "variables.name == 'endpoints1'",
    71  			expectedResult: true,
    72  		},
    73  		{
    74  			name: "early compile error",
    75  			variables: []NamedExpressionAccessor{
    76  				&testVariable{
    77  					name:       "name",
    78  					expression: "1 == '1'", // won't compile
    79  				},
    80  			},
    81  			attributes:           endpointCreateAttributes(),
    82  			expression:           "variables.name == 'endpoints1'",
    83  			expectErr:            true,
    84  			expectedErrorMessage: `found no matching overload for '_==_' applied to '(int, string)'`,
    85  		},
    86  		{
    87  			name: "delayed eval error",
    88  			variables: []NamedExpressionAccessor{
    89  				&testVariable{
    90  					name:       "count",
    91  					expression: "object.subsets[114514].addresses.size()", // array index out of bound
    92  				},
    93  			},
    94  			attributes:           endpointCreateAttributes(),
    95  			expression:           "variables.count == 810",
    96  			expectErr:            true,
    97  			expectedErrorMessage: `composited variable "count" fails to evaluate: index out of bounds: 114514`,
    98  		},
    99  		{
   100  			name: "out of budget during lazy evaluation",
   101  			variables: []NamedExpressionAccessor{
   102  				&testVariable{
   103  					name:       "name",
   104  					expression: "object.metadata.name", // cost = 3
   105  				},
   106  			},
   107  			attributes:           endpointCreateAttributes(),
   108  			expression:           "variables.name == 'endpoints1'", // cost = 3
   109  			expectedResult:       true,
   110  			runtimeCostBudget:    4, // enough for main variable but not for entire expression
   111  			expectErr:            true,
   112  			expectedErrorMessage: "running out of cost budget",
   113  		},
   114  		{
   115  			name: "lazy evaluation, budget counts only once",
   116  			variables: []NamedExpressionAccessor{
   117  				&testVariable{
   118  					name:       "name",
   119  					expression: "object.metadata.name", // cost = 3
   120  				},
   121  			},
   122  			attributes:        endpointCreateAttributes(),
   123  			expression:        "variables.name == 'endpoints1' && variables.name == 'endpoints1' ", // cost = 7
   124  			expectedResult:    true,
   125  			runtimeCostBudget: 10, // enough for one lazy evaluation but not two, should pass
   126  		},
   127  		{
   128  			name: "single boolean variable in expression",
   129  			variables: []NamedExpressionAccessor{
   130  				&testVariable{
   131  					name:       "fortuneTelling",
   132  					expression: "true",
   133  				},
   134  			},
   135  			attributes:     endpointCreateAttributes(),
   136  			expression:     "variables.fortuneTelling",
   137  			expectedResult: true,
   138  		},
   139  		{
   140  			name: "variable of a list",
   141  			variables: []NamedExpressionAccessor{
   142  				&testVariable{
   143  					name:       "list",
   144  					expression: "[1, 2, 3, 4]",
   145  				},
   146  			},
   147  			attributes:     endpointCreateAttributes(),
   148  			expression:     "variables.list.sum() == 10",
   149  			expectedResult: true,
   150  		},
   151  		{
   152  			name: "variable of a map",
   153  			variables: []NamedExpressionAccessor{
   154  				&testVariable{
   155  					name:       "dict",
   156  					expression: `{"foo": "bar"}`,
   157  				},
   158  			},
   159  			attributes:     endpointCreateAttributes(),
   160  			expression:     "variables.dict['foo'].contains('bar')",
   161  			expectedResult: true,
   162  		},
   163  		{
   164  			name: "variable of a list but confused as a map",
   165  			variables: []NamedExpressionAccessor{
   166  				&testVariable{
   167  					name:       "list",
   168  					expression: "[1, 2, 3, 4]",
   169  				},
   170  			},
   171  			attributes:           endpointCreateAttributes(),
   172  			expression:           "variables.list['invalid'] == 'invalid'",
   173  			expectErr:            true,
   174  			expectedErrorMessage: "found no matching overload for '_[_]' applied to '(list(int), string)'",
   175  		},
   176  		{
   177  			name: "list of strings, but element is confused as an integer",
   178  			variables: []NamedExpressionAccessor{
   179  				&testVariable{
   180  					name:       "list",
   181  					expression: "['1', '2', '3', '4']",
   182  				},
   183  			},
   184  			attributes:           endpointCreateAttributes(),
   185  			expression:           "variables.list[0] == 1",
   186  			expectErr:            true,
   187  			expectedErrorMessage: "found no matching overload for '_==_' applied to '(string, int)'",
   188  		},
   189  		{
   190  			name: "with strictCostEnforcement on: exceeds cost budget",
   191  			variables: []NamedExpressionAccessor{
   192  				&testVariable{
   193  					name:       "dict",
   194  					expression: "'abc 123 def 123'.split(' ')",
   195  				},
   196  			},
   197  			attributes:            endpointCreateAttributes(),
   198  			expression:            "size(variables.dict) > 0",
   199  			expectErr:             true,
   200  			expectedErrorMessage:  "validation failed due to running out of cost budget, no further validation rules will be run",
   201  			runtimeCostBudget:     5,
   202  			strictCostEnforcement: true,
   203  		},
   204  		{
   205  			name: "with strictCostEnforcement off: not exceed cost budget",
   206  			variables: []NamedExpressionAccessor{
   207  				&testVariable{
   208  					name:       "dict",
   209  					expression: "'abc 123 def 123'.split(' ')",
   210  				},
   211  			},
   212  			attributes:            endpointCreateAttributes(),
   213  			expression:            "size(variables.dict) > 0",
   214  			expectedResult:        true,
   215  			runtimeCostBudget:     5,
   216  			strictCostEnforcement: false,
   217  		},
   218  	}
   219  	for _, tc := range cases {
   220  		t.Run(tc.name, func(t *testing.T) {
   221  			compiler, err := NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.strictCostEnforcement))
   222  			if err != nil {
   223  				t.Fatal(err)
   224  			}
   225  			compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
   226  			validations := []ExpressionAccessor{&condition{Expression: tc.expression}}
   227  			f := compiler.Compile(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
   228  			versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
   229  			if err != nil {
   230  				t.Fatal(err)
   231  			}
   232  			optionalVars := OptionalVariableBindings{}
   233  			costBudget := tc.runtimeCostBudget
   234  			if costBudget == 0 {
   235  				costBudget = celconfig.RuntimeCELCostBudget
   236  			}
   237  			result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, v1.GroupVersionResource(tc.attributes.GetResource()), v1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, nil, costBudget)
   238  			if !tc.expectErr && err != nil {
   239  				t.Fatalf("failed evaluation: %v", err)
   240  			}
   241  			if !tc.expectErr && len(result) == 0 {
   242  				t.Fatal("unexpected empty result")
   243  			}
   244  			if err == nil {
   245  				err = result[0].Error
   246  			}
   247  			if tc.expectErr {
   248  				if err == nil {
   249  					t.Fatal("unexpected no error")
   250  				}
   251  				if !strings.Contains(err.Error(), tc.expectedErrorMessage) {
   252  					t.Errorf("expected error to contain %q but got %s", tc.expectedErrorMessage, err.Error())
   253  				}
   254  				return
   255  			}
   256  			if err != nil {
   257  				t.Fatalf("failed validation: %v", result[0].Error)
   258  			}
   259  			if tc.expectedResult != result[0].EvalResult.Value() {
   260  				t.Errorf("wrong result: expected %v but got %v", tc.expectedResult, result)
   261  			}
   262  
   263  		})
   264  	}
   265  }