k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/validating/caching_authorizer_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 validating 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "testing" 24 25 "k8s.io/apimachinery/pkg/fields" 26 "k8s.io/apimachinery/pkg/labels" 27 "k8s.io/apiserver/pkg/authentication/user" 28 "k8s.io/apiserver/pkg/authorization/authorizer" 29 genericfeatures "k8s.io/apiserver/pkg/features" 30 utilfeature "k8s.io/apiserver/pkg/util/feature" 31 featuregatetesting "k8s.io/component-base/featuregate/testing" 32 ) 33 34 func mustParseLabelSelector(str string) labels.Requirements { 35 ret, err := labels.Parse(str) 36 if err != nil { 37 panic(err) 38 } 39 retRequirements, _ /*selectable*/ := ret.Requirements() 40 return retRequirements 41 } 42 43 func TestCachingAuthorizer(t *testing.T) { 44 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true) 45 46 type result struct { 47 decision authorizer.Decision 48 reason string 49 error error 50 } 51 52 type invocation struct { 53 attributes authorizer.Attributes 54 expected result 55 } 56 57 for _, tc := range []struct { 58 name string 59 calls []invocation 60 backend []result 61 }{ 62 { 63 name: "hit", 64 calls: []invocation{ 65 { 66 attributes: authorizer.AttributesRecord{Name: "test name"}, 67 expected: result{ 68 decision: authorizer.DecisionAllow, 69 reason: "test reason", 70 error: fmt.Errorf("test error"), 71 }, 72 }, 73 { 74 attributes: authorizer.AttributesRecord{Name: "test name"}, 75 expected: result{ 76 decision: authorizer.DecisionAllow, 77 reason: "test reason", 78 error: fmt.Errorf("test error"), 79 }, 80 }, 81 }, 82 backend: []result{ 83 { 84 decision: authorizer.DecisionAllow, 85 reason: "test reason", 86 error: fmt.Errorf("test error"), 87 }, 88 }, 89 }, 90 { 91 name: "hit with differently-ordered groups", 92 calls: []invocation{ 93 { 94 attributes: authorizer.AttributesRecord{ 95 User: &user.DefaultInfo{ 96 Groups: []string{"a", "b", "c"}, 97 }, 98 }, 99 expected: result{ 100 decision: authorizer.DecisionAllow, 101 reason: "test reason", 102 error: fmt.Errorf("test error"), 103 }, 104 }, 105 { 106 attributes: authorizer.AttributesRecord{ 107 User: &user.DefaultInfo{ 108 Groups: []string{"c", "b", "a"}, 109 }, 110 }, 111 expected: result{ 112 decision: authorizer.DecisionAllow, 113 reason: "test reason", 114 error: fmt.Errorf("test error"), 115 }, 116 }, 117 }, 118 backend: []result{ 119 { 120 decision: authorizer.DecisionAllow, 121 reason: "test reason", 122 error: fmt.Errorf("test error"), 123 }, 124 }, 125 }, 126 { 127 name: "hit with differently-ordered extra", 128 calls: []invocation{ 129 { 130 attributes: authorizer.AttributesRecord{ 131 User: &user.DefaultInfo{ 132 Extra: map[string][]string{ 133 "k": {"a", "b", "c"}, 134 }, 135 }, 136 }, 137 expected: result{ 138 decision: authorizer.DecisionAllow, 139 reason: "test reason", 140 error: fmt.Errorf("test error"), 141 }, 142 }, 143 { 144 attributes: authorizer.AttributesRecord{ 145 User: &user.DefaultInfo{ 146 Extra: map[string][]string{ 147 "k": {"c", "b", "a"}, 148 }, 149 }, 150 }, 151 expected: result{ 152 decision: authorizer.DecisionAllow, 153 reason: "test reason", 154 error: fmt.Errorf("test error"), 155 }, 156 }, 157 }, 158 backend: []result{ 159 { 160 decision: authorizer.DecisionAllow, 161 reason: "test reason", 162 error: fmt.Errorf("test error"), 163 }, 164 }, 165 }, 166 { 167 name: "miss due to different name", 168 calls: []invocation{ 169 { 170 attributes: authorizer.AttributesRecord{Name: "alpha"}, 171 expected: result{ 172 decision: authorizer.DecisionAllow, 173 reason: "test reason alpha", 174 error: fmt.Errorf("test error alpha"), 175 }, 176 }, 177 { 178 attributes: authorizer.AttributesRecord{Name: "beta"}, 179 expected: result{ 180 decision: authorizer.DecisionDeny, 181 reason: "test reason beta", 182 error: fmt.Errorf("test error beta"), 183 }, 184 }, 185 }, 186 backend: []result{ 187 { 188 decision: authorizer.DecisionAllow, 189 reason: "test reason alpha", 190 error: fmt.Errorf("test error alpha"), 191 }, 192 { 193 decision: authorizer.DecisionDeny, 194 reason: "test reason beta", 195 error: fmt.Errorf("test error beta"), 196 }, 197 }, 198 }, 199 { 200 name: "miss due to different user", 201 calls: []invocation{ 202 { 203 attributes: authorizer.AttributesRecord{ 204 User: &user.DefaultInfo{Name: "alpha"}, 205 }, 206 expected: result{ 207 decision: authorizer.DecisionAllow, 208 reason: "test reason alpha", 209 error: fmt.Errorf("test error alpha"), 210 }, 211 }, 212 { 213 attributes: authorizer.AttributesRecord{ 214 User: &user.DefaultInfo{Name: "beta"}, 215 }, 216 expected: result{ 217 decision: authorizer.DecisionDeny, 218 reason: "test reason beta", 219 error: fmt.Errorf("test error beta"), 220 }, 221 }, 222 }, 223 backend: []result{ 224 { 225 decision: authorizer.DecisionAllow, 226 reason: "test reason alpha", 227 error: fmt.Errorf("test error alpha"), 228 }, 229 { 230 decision: authorizer.DecisionDeny, 231 reason: "test reason beta", 232 error: fmt.Errorf("test error beta"), 233 }, 234 }, 235 }, 236 { 237 name: "honor good field selector", 238 calls: []invocation{ 239 { 240 attributes: authorizer.AttributesRecord{ 241 Name: "test name", 242 FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(), 243 }, 244 expected: result{ 245 decision: authorizer.DecisionAllow, 246 reason: "test reason", 247 error: fmt.Errorf("test error"), 248 }, 249 }, 250 { 251 attributes: authorizer.AttributesRecord{ 252 Name: "test name", 253 }, 254 expected: result{ 255 decision: authorizer.DecisionAllow, 256 reason: "test reason 2", 257 error: fmt.Errorf("test error 2"), 258 }, 259 }, 260 { 261 // now this should be cached 262 attributes: authorizer.AttributesRecord{ 263 Name: "test name", 264 FieldSelectorRequirements: fields.ParseSelectorOrDie("foo=bar").Requirements(), 265 }, 266 expected: result{ 267 decision: authorizer.DecisionAllow, 268 reason: "test reason", 269 error: fmt.Errorf("test error"), 270 }, 271 }, 272 }, 273 backend: []result{ 274 { 275 decision: authorizer.DecisionAllow, 276 reason: "test reason", 277 error: fmt.Errorf("test error"), 278 }, 279 { 280 decision: authorizer.DecisionAllow, 281 reason: "test reason 2", 282 error: fmt.Errorf("test error 2"), 283 }, 284 }, 285 }, 286 { 287 name: "ignore malformed field selector first", 288 calls: []invocation{ 289 { 290 attributes: authorizer.AttributesRecord{ 291 Name: "test name", 292 FieldSelectorParsingErr: errors.New("malformed"), 293 }, 294 expected: result{ 295 decision: authorizer.DecisionAllow, 296 reason: "test reason", 297 error: fmt.Errorf("test error"), 298 }, 299 }, 300 { 301 // notice that this does not have the malformed field selector. 302 // it should use the cached result 303 attributes: authorizer.AttributesRecord{ 304 Name: "test name", 305 }, 306 expected: result{ 307 decision: authorizer.DecisionAllow, 308 reason: "test reason", 309 error: fmt.Errorf("test error"), 310 }, 311 }, 312 }, 313 backend: []result{ 314 { 315 decision: authorizer.DecisionAllow, 316 reason: "test reason", 317 error: fmt.Errorf("test error"), 318 }, 319 }, 320 }, 321 { 322 name: "ignore malformed field selector second", 323 calls: []invocation{ 324 { 325 attributes: authorizer.AttributesRecord{ 326 Name: "test name", 327 }, 328 expected: result{ 329 decision: authorizer.DecisionAllow, 330 reason: "test reason", 331 error: fmt.Errorf("test error"), 332 }, 333 }, 334 { 335 // this should use the broader cached value because the selector will be ignored 336 attributes: authorizer.AttributesRecord{ 337 Name: "test name", 338 FieldSelectorParsingErr: errors.New("malformed"), 339 }, 340 expected: result{ 341 decision: authorizer.DecisionAllow, 342 reason: "test reason", 343 error: fmt.Errorf("test error"), 344 }, 345 }, 346 }, 347 backend: []result{ 348 { 349 decision: authorizer.DecisionAllow, 350 reason: "test reason", 351 error: fmt.Errorf("test error"), 352 }, 353 }, 354 }, 355 356 { 357 name: "honor good label selector", 358 calls: []invocation{ 359 { 360 attributes: authorizer.AttributesRecord{ 361 Name: "test name", 362 LabelSelectorRequirements: mustParseLabelSelector("foo=bar"), 363 }, 364 expected: result{ 365 decision: authorizer.DecisionAllow, 366 reason: "test reason", 367 error: fmt.Errorf("test error"), 368 }, 369 }, 370 { 371 attributes: authorizer.AttributesRecord{ 372 Name: "test name", 373 }, 374 expected: result{ 375 decision: authorizer.DecisionAllow, 376 reason: "test reason 2", 377 error: fmt.Errorf("test error 2"), 378 }, 379 }, 380 { 381 // now this should be cached 382 attributes: authorizer.AttributesRecord{ 383 Name: "test name", 384 LabelSelectorRequirements: mustParseLabelSelector("foo=bar"), 385 }, 386 expected: result{ 387 decision: authorizer.DecisionAllow, 388 reason: "test reason", 389 error: fmt.Errorf("test error"), 390 }, 391 }, 392 { 393 attributes: authorizer.AttributesRecord{ 394 Name: "test name", 395 LabelSelectorRequirements: mustParseLabelSelector("diff=zero"), 396 }, 397 expected: result{ 398 decision: authorizer.DecisionAllow, 399 reason: "test reason 3", 400 error: fmt.Errorf("test error 3"), 401 }, 402 }, 403 }, 404 backend: []result{ 405 { 406 decision: authorizer.DecisionAllow, 407 reason: "test reason", 408 error: fmt.Errorf("test error"), 409 }, 410 { 411 decision: authorizer.DecisionAllow, 412 reason: "test reason 2", 413 error: fmt.Errorf("test error 2"), 414 }, 415 { 416 decision: authorizer.DecisionAllow, 417 reason: "test reason 3", 418 error: fmt.Errorf("test error 3"), 419 }, 420 }, 421 }, 422 { 423 name: "ignore malformed label selector first", 424 calls: []invocation{ 425 { 426 attributes: authorizer.AttributesRecord{ 427 Name: "test name", 428 LabelSelectorParsingErr: errors.New("malformed mess"), 429 }, 430 expected: result{ 431 decision: authorizer.DecisionAllow, 432 reason: "test reason", 433 error: fmt.Errorf("test error"), 434 }, 435 }, 436 { 437 // notice that this does not have the malformed field selector. 438 // it should use the cached result 439 attributes: authorizer.AttributesRecord{ 440 Name: "test name", 441 }, 442 expected: result{ 443 decision: authorizer.DecisionAllow, 444 reason: "test reason", 445 error: fmt.Errorf("test error"), 446 }, 447 }, 448 }, 449 backend: []result{ 450 { 451 decision: authorizer.DecisionAllow, 452 reason: "test reason", 453 error: fmt.Errorf("test error"), 454 }, 455 }, 456 }, 457 { 458 name: "ignore malformed label selector second", 459 calls: []invocation{ 460 { 461 attributes: authorizer.AttributesRecord{ 462 Name: "test name", 463 }, 464 expected: result{ 465 decision: authorizer.DecisionAllow, 466 reason: "test reason", 467 error: fmt.Errorf("test error"), 468 }, 469 }, 470 { 471 // this should use the broader cached value because the selector will be ignored 472 attributes: authorizer.AttributesRecord{ 473 Name: "test name", 474 LabelSelectorParsingErr: errors.New("malformed mess"), 475 }, 476 expected: result{ 477 decision: authorizer.DecisionAllow, 478 reason: "test reason", 479 error: fmt.Errorf("test error"), 480 }, 481 }, 482 }, 483 backend: []result{ 484 { 485 decision: authorizer.DecisionAllow, 486 reason: "test reason", 487 error: fmt.Errorf("test error"), 488 }, 489 }, 490 }, 491 } { 492 t.Run(tc.name, func(t *testing.T) { 493 var misses int 494 frontend := newCachingAuthorizer(func() authorizer.Authorizer { 495 return authorizer.AuthorizerFunc(func(_ context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) { 496 if misses >= len(tc.backend) { 497 t.Fatalf("got more than expected %d backend invocations", len(tc.backend)) 498 } 499 result := tc.backend[misses] 500 misses++ 501 return result.decision, result.reason, result.error 502 }) 503 }()) 504 505 for i, invocation := range tc.calls { 506 decision, reason, err := frontend.Authorize(context.TODO(), invocation.attributes) 507 if decision != invocation.expected.decision { 508 t.Errorf("(call %d of %d) expected decision %v, got %v", i+1, len(tc.calls), invocation.expected.decision, decision) 509 } 510 if reason != invocation.expected.reason { 511 t.Errorf("(call %d of %d) expected reason %q, got %q", i+1, len(tc.calls), invocation.expected.reason, reason) 512 } 513 if err.Error() != invocation.expected.error.Error() { 514 t.Errorf("(call %d of %d) expected error %q, got %q", i+1, len(tc.calls), invocation.expected.error.Error(), err.Error()) 515 } 516 } 517 518 if len(tc.backend) > misses { 519 t.Errorf("expected %d backend invocations, got %d", len(tc.backend), misses) 520 } 521 }) 522 } 523 }