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 }