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 }