k8s.io/apiserver@v0.31.1/plugin/pkg/authorizer/webhook/webhook_v1_test.go (about) 1 /* 2 Copyright 2016 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 webhook 18 19 import ( 20 "context" 21 "crypto/tls" 22 "crypto/x509" 23 "encoding/json" 24 "fmt" 25 "io/ioutil" 26 "net/http" 27 "net/http/httptest" 28 "net/url" 29 "os" 30 "path/filepath" 31 "reflect" 32 "strings" 33 "testing" 34 "text/template" 35 "time" 36 37 utiltesting "k8s.io/client-go/util/testing" 38 39 "github.com/google/go-cmp/cmp" 40 41 authorizationv1 "k8s.io/api/authorization/v1" 42 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 43 "k8s.io/apimachinery/pkg/fields" 44 "k8s.io/apimachinery/pkg/labels" 45 "k8s.io/apimachinery/pkg/selection" 46 "k8s.io/apimachinery/pkg/util/wait" 47 "k8s.io/apiserver/pkg/apis/apiserver" 48 "k8s.io/apiserver/pkg/authentication/user" 49 "k8s.io/apiserver/pkg/authorization/authorizer" 50 celmetrics "k8s.io/apiserver/pkg/authorization/cel" 51 "k8s.io/apiserver/pkg/features" 52 utilfeature "k8s.io/apiserver/pkg/util/feature" 53 webhookutil "k8s.io/apiserver/pkg/util/webhook" 54 "k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics" 55 v1 "k8s.io/client-go/tools/clientcmd/api/v1" 56 featuregatetesting "k8s.io/component-base/featuregate/testing" 57 "k8s.io/component-base/metrics/legacyregistry" 58 "k8s.io/component-base/metrics/testutil" 59 ) 60 61 var testRetryBackoff = wait.Backoff{ 62 Duration: 5 * time.Millisecond, 63 Factor: 1.5, 64 Jitter: 0.2, 65 Steps: 5, 66 } 67 68 func TestV1NewFromConfig(t *testing.T) { 69 dir, err := ioutil.TempDir("", "") 70 if err != nil { 71 t.Fatal(err) 72 } 73 defer os.RemoveAll(dir) 74 75 data := struct { 76 CA string 77 Cert string 78 Key string 79 }{ 80 CA: filepath.Join(dir, "ca.pem"), 81 Cert: filepath.Join(dir, "clientcert.pem"), 82 Key: filepath.Join(dir, "clientkey.pem"), 83 } 84 85 files := []struct { 86 name string 87 data []byte 88 }{ 89 {data.CA, caCert}, 90 {data.Cert, clientCert}, 91 {data.Key, clientKey}, 92 } 93 for _, file := range files { 94 if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil { 95 t.Fatal(err) 96 } 97 } 98 99 tests := []struct { 100 msg string 101 configTmpl string 102 wantErr bool 103 }{ 104 { 105 msg: "a single cluster and single user", 106 configTmpl: ` 107 clusters: 108 - cluster: 109 certificate-authority: {{ .CA }} 110 server: https://authz.example.com 111 name: foobar 112 users: 113 - name: a cluster 114 user: 115 client-certificate: {{ .Cert }} 116 client-key: {{ .Key }} 117 `, 118 wantErr: true, 119 }, 120 { 121 msg: "multiple clusters with no context", 122 configTmpl: ` 123 clusters: 124 - cluster: 125 certificate-authority: {{ .CA }} 126 server: https://authz.example.com 127 name: foobar 128 - cluster: 129 certificate-authority: a bad certificate path 130 server: https://authz.example.com 131 name: barfoo 132 users: 133 - name: a name 134 user: 135 client-certificate: {{ .Cert }} 136 client-key: {{ .Key }} 137 `, 138 wantErr: true, 139 }, 140 { 141 msg: "multiple clusters with a context", 142 configTmpl: ` 143 clusters: 144 - cluster: 145 certificate-authority: a bad certificate path 146 server: https://authz.example.com 147 name: foobar 148 - cluster: 149 certificate-authority: {{ .CA }} 150 server: https://authz.example.com 151 name: barfoo 152 users: 153 - name: a name 154 user: 155 client-certificate: {{ .Cert }} 156 client-key: {{ .Key }} 157 contexts: 158 - name: default 159 context: 160 cluster: barfoo 161 user: a name 162 current-context: default 163 `, 164 wantErr: false, 165 }, 166 { 167 msg: "cluster with bad certificate path specified", 168 configTmpl: ` 169 clusters: 170 - cluster: 171 certificate-authority: a bad certificate path 172 server: https://authz.example.com 173 name: foobar 174 - cluster: 175 certificate-authority: {{ .CA }} 176 server: https://authz.example.com 177 name: barfoo 178 users: 179 - name: a name 180 user: 181 client-certificate: {{ .Cert }} 182 client-key: {{ .Key }} 183 contexts: 184 - name: default 185 context: 186 cluster: foobar 187 user: a name 188 current-context: default 189 `, 190 wantErr: true, 191 }, 192 } 193 194 for _, tt := range tests { 195 // Use a closure so defer statements trigger between loop iterations. 196 err := func() error { 197 tempfile, err := ioutil.TempFile("", "") 198 if err != nil { 199 return err 200 } 201 p := tempfile.Name() 202 defer utiltesting.CloseAndRemove(t, tempfile) 203 204 tmpl, err := template.New("test").Parse(tt.configTmpl) 205 if err != nil { 206 return fmt.Errorf("failed to parse test template: %v", err) 207 } 208 if err := tmpl.Execute(tempfile, data); err != nil { 209 return fmt.Errorf("failed to execute test template: %v", err) 210 } 211 // Create a new authorizer 212 clientConfig, err := webhookutil.LoadKubeconfig(p, nil) 213 if err != nil { 214 return err 215 } 216 sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1", testRetryBackoff) 217 if err != nil { 218 return fmt.Errorf("error building sar client: %v", err) 219 } 220 _, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []apiserver.WebhookMatchCondition{}, noopAuthorizerMetrics(), "") 221 return err 222 }() 223 if err != nil && !tt.wantErr { 224 t.Errorf("failed to load plugin from config %q: %v", tt.msg, err) 225 } 226 if err == nil && tt.wantErr { 227 t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg) 228 } 229 } 230 } 231 232 // V1Service mocks a remote service. 233 type V1Service interface { 234 Review(*authorizationv1.SubjectAccessReview) 235 HTTPStatusCode() int 236 } 237 238 // NewV1TestServer wraps a V1Service as an httptest.Server. 239 func NewV1TestServer(s V1Service, cert, key, caCert []byte) (*httptest.Server, error) { 240 const webhookPath = "/testserver" 241 var tlsConfig *tls.Config 242 if cert != nil { 243 cert, err := tls.X509KeyPair(cert, key) 244 if err != nil { 245 return nil, err 246 } 247 tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} 248 } 249 250 if caCert != nil { 251 rootCAs := x509.NewCertPool() 252 rootCAs.AppendCertsFromPEM(caCert) 253 if tlsConfig == nil { 254 tlsConfig = &tls.Config{} 255 } 256 tlsConfig.ClientCAs = rootCAs 257 tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 258 } 259 260 serveHTTP := func(w http.ResponseWriter, r *http.Request) { 261 if r.Method != "POST" { 262 http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed) 263 return 264 } 265 if r.URL.Path != webhookPath { 266 http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound) 267 return 268 } 269 270 var review authorizationv1.SubjectAccessReview 271 bodyData, _ := ioutil.ReadAll(r.Body) 272 if err := json.Unmarshal(bodyData, &review); err != nil { 273 http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest) 274 return 275 } 276 277 // ensure we received the serialized review as expected 278 if review.APIVersion != "authorization.k8s.io/v1" { 279 http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest) 280 return 281 } 282 // once we have a successful request, always call the review to record that we were called 283 s.Review(&review) 284 if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 { 285 http.Error(w, "HTTP Error", s.HTTPStatusCode()) 286 return 287 } 288 type status struct { 289 Allowed bool `json:"allowed"` 290 Reason string `json:"reason"` 291 EvaluationError string `json:"evaluationError"` 292 } 293 resp := struct { 294 APIVersion string `json:"apiVersion"` 295 Status status `json:"status"` 296 }{ 297 APIVersion: authorizationv1.SchemeGroupVersion.String(), 298 Status: status{review.Status.Allowed, review.Status.Reason, review.Status.EvaluationError}, 299 } 300 w.Header().Set("Content-Type", "application/json") 301 json.NewEncoder(w).Encode(resp) 302 } 303 304 server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP)) 305 server.TLS = tlsConfig 306 server.StartTLS() 307 308 // Adjust the path to point to our custom path 309 serverURL, _ := url.Parse(server.URL) 310 serverURL.Path = webhookPath 311 server.URL = serverURL.String() 312 313 return server, nil 314 } 315 316 // A service that can be set to allow all or deny all authorization requests. 317 type mockV1Service struct { 318 allow bool 319 statusCode int 320 called int 321 322 // reviewHook is called just before returning from the Review() method 323 reviewHook func(*authorizationv1.SubjectAccessReview) 324 } 325 326 func (m *mockV1Service) Review(r *authorizationv1.SubjectAccessReview) { 327 m.called++ 328 r.Status.Allowed = m.allow 329 330 if m.reviewHook != nil { 331 m.reviewHook(r) 332 } 333 } 334 func (m *mockV1Service) Allow() { m.allow = true } 335 func (m *mockV1Service) Deny() { m.allow = false } 336 func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode } 337 338 // newV1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load 339 // a new WebhookAuthorizer from it. 340 func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, metrics metrics.AuthorizerMetrics, expressions []apiserver.WebhookMatchCondition, authzName string) (*WebhookAuthorizer, error) { 341 tempfile, err := ioutil.TempFile("", "") 342 if err != nil { 343 return nil, err 344 } 345 p := tempfile.Name() 346 defer os.Remove(p) 347 config := v1.Config{ 348 Clusters: []v1.NamedCluster{ 349 { 350 Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca}, 351 }, 352 }, 353 AuthInfos: []v1.NamedAuthInfo{ 354 { 355 AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey}, 356 }, 357 }, 358 } 359 if err := json.NewEncoder(tempfile).Encode(config); err != nil { 360 return nil, err 361 } 362 clientConfig, err := webhookutil.LoadKubeconfig(p, nil) 363 if err != nil { 364 return nil, err 365 } 366 sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1", testRetryBackoff) 367 if err != nil { 368 return nil, fmt.Errorf("error building sar client: %v", err) 369 } 370 return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, expressions, metrics, authzName) 371 } 372 373 func TestV1TLSConfig(t *testing.T) { 374 tests := []struct { 375 test string 376 clientCert, clientKey, clientCA []byte 377 serverCert, serverKey, serverCA []byte 378 wantAuth, wantErr bool 379 }{ 380 { 381 test: "TLS setup between client and server", 382 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 383 serverCert: serverCert, serverKey: serverKey, serverCA: caCert, 384 wantAuth: true, 385 }, 386 { 387 test: "Server does not require client auth", 388 clientCA: caCert, 389 serverCert: serverCert, serverKey: serverKey, 390 wantAuth: true, 391 }, 392 { 393 test: "Server does not require client auth, client provides it", 394 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 395 serverCert: serverCert, serverKey: serverKey, 396 wantAuth: true, 397 }, 398 { 399 test: "Client does not trust server", 400 clientCert: clientCert, clientKey: clientKey, 401 serverCert: serverCert, serverKey: serverKey, 402 wantErr: true, 403 }, 404 { 405 test: "Server does not trust client", 406 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 407 serverCert: serverCert, serverKey: serverKey, serverCA: badCACert, 408 wantErr: true, 409 }, 410 { 411 // Plugin does not support insecure configurations. 412 test: "Server is using insecure connection", 413 wantErr: true, 414 }, 415 } 416 for _, tt := range tests { 417 // Use a closure so defer statements trigger between loop iterations. 418 func() { 419 service := new(mockV1Service) 420 service.statusCode = 200 421 422 server, err := NewV1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA) 423 if err != nil { 424 t.Errorf("%s: failed to create server: %v", tt.test, err) 425 return 426 } 427 defer server.Close() 428 429 wh, err := newV1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, noopAuthorizerMetrics(), []apiserver.WebhookMatchCondition{}, "") 430 if err != nil { 431 t.Errorf("%s: failed to create client: %v", tt.test, err) 432 return 433 } 434 435 attr := authorizer.AttributesRecord{User: &user.DefaultInfo{}} 436 437 // Allow all and see if we get an error. 438 service.Allow() 439 decision, _, err := wh.Authorize(context.Background(), attr) 440 if tt.wantAuth { 441 if decision != authorizer.DecisionAllow { 442 t.Errorf("expected successful authorization") 443 } 444 } else { 445 if decision == authorizer.DecisionAllow { 446 t.Errorf("expected failed authorization") 447 } 448 } 449 if tt.wantErr { 450 if err == nil { 451 t.Errorf("expected error making authorization request: %v", err) 452 } 453 return 454 } 455 if err != nil { 456 t.Errorf("%s: failed to authorize with AllowAll policy: %v", tt.test, err) 457 return 458 } 459 460 service.Deny() 461 if decision, _, _ := wh.Authorize(context.Background(), attr); decision == authorizer.DecisionAllow { 462 t.Errorf("%s: incorrectly authorized with DenyAll policy", tt.test) 463 } 464 }() 465 } 466 } 467 468 // recorderV1Service records all access review requests. 469 type recorderV1Service struct { 470 last authorizationv1.SubjectAccessReview 471 err error 472 } 473 474 func (rec *recorderV1Service) Review(r *authorizationv1.SubjectAccessReview) { 475 rec.last = authorizationv1.SubjectAccessReview{} 476 rec.last = *r 477 r.Status.Allowed = true 478 } 479 480 func (rec *recorderV1Service) Last() (authorizationv1.SubjectAccessReview, error) { 481 return rec.last, rec.err 482 } 483 484 func (rec *recorderV1Service) HTTPStatusCode() int { return 200 } 485 486 func TestV1Webhook(t *testing.T) { 487 serv := new(recorderV1Service) 488 s, err := NewV1TestServer(serv, serverCert, serverKey, caCert) 489 if err != nil { 490 t.Fatal(err) 491 } 492 defer s.Close() 493 494 wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), []apiserver.WebhookMatchCondition{}, "") 495 if err != nil { 496 t.Fatal(err) 497 } 498 499 expTypeMeta := metav1.TypeMeta{ 500 APIVersion: "authorization.k8s.io/v1", 501 Kind: "SubjectAccessReview", 502 } 503 504 tests := []struct { 505 attr authorizer.Attributes 506 want authorizationv1.SubjectAccessReview 507 }{ 508 { 509 attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}}, 510 want: authorizationv1.SubjectAccessReview{ 511 TypeMeta: expTypeMeta, 512 Spec: authorizationv1.SubjectAccessReviewSpec{ 513 NonResourceAttributes: &authorizationv1.NonResourceAttributes{}, 514 }, 515 }, 516 }, 517 { 518 attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}}, 519 want: authorizationv1.SubjectAccessReview{ 520 TypeMeta: expTypeMeta, 521 Spec: authorizationv1.SubjectAccessReviewSpec{ 522 User: "jane", 523 NonResourceAttributes: &authorizationv1.NonResourceAttributes{}, 524 }, 525 }, 526 }, 527 { 528 attr: authorizer.AttributesRecord{ 529 User: &user.DefaultInfo{ 530 Name: "jane", 531 UID: "1", 532 Groups: []string{"group1", "group2"}, 533 }, 534 Verb: "GET", 535 Namespace: "kittensandponies", 536 APIGroup: "group3", 537 APIVersion: "v7beta3", 538 Resource: "pods", 539 Subresource: "proxy", 540 Name: "my-pod", 541 ResourceRequest: true, 542 Path: "/foo", 543 }, 544 want: authorizationv1.SubjectAccessReview{ 545 TypeMeta: expTypeMeta, 546 Spec: authorizationv1.SubjectAccessReviewSpec{ 547 User: "jane", 548 UID: "1", 549 Groups: []string{"group1", "group2"}, 550 ResourceAttributes: &authorizationv1.ResourceAttributes{ 551 Verb: "GET", 552 Namespace: "kittensandponies", 553 Group: "group3", 554 Version: "v7beta3", 555 Resource: "pods", 556 Subresource: "proxy", 557 Name: "my-pod", 558 }, 559 }, 560 }, 561 }, 562 } 563 564 for i, tt := range tests { 565 decision, _, err := wh.Authorize(context.Background(), tt.attr) 566 if err != nil { 567 t.Fatal(err) 568 } 569 if decision != authorizer.DecisionAllow { 570 t.Errorf("case %d: authorization failed", i) 571 continue 572 } 573 574 gotAttr, err := serv.Last() 575 if err != nil { 576 t.Errorf("case %d: failed to deserialize webhook request: %v", i, err) 577 continue 578 } 579 if !reflect.DeepEqual(gotAttr, tt.want) { 580 t.Errorf("case %d: got != want:\n%s", i, cmp.Diff(gotAttr, tt.want)) 581 } 582 } 583 } 584 585 // TestWebhookCache verifies that error responses from the server are not 586 // cached, but successful responses are. 587 func TestV1WebhookCache(t *testing.T) { 588 serv := new(mockV1Service) 589 s, err := NewV1TestServer(serv, serverCert, serverKey, caCert) 590 if err != nil { 591 t.Fatal(err) 592 } 593 defer s.Close() 594 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true) 595 expressions := []apiserver.WebhookMatchCondition{ 596 { 597 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", 598 }, 599 } 600 // Create an authorizer that caches successful responses "forever" (100 days). 601 wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, noopAuthorizerMetrics(), expressions, "") 602 if err != nil { 603 t.Fatal(err) 604 } 605 606 aliceAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "alice"}, ResourceRequest: true, Namespace: "kittensandponies"} 607 bobAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "bob"}, ResourceRequest: true, Namespace: "kittensandponies"} 608 aliceRidiculousAttr := authorizer.AttributesRecord{ 609 User: &user.DefaultInfo{Name: "alice"}, 610 ResourceRequest: true, 611 Verb: strings.Repeat("v", 2000), 612 APIGroup: strings.Repeat("g", 2000), 613 APIVersion: strings.Repeat("a", 2000), 614 Resource: strings.Repeat("r", 2000), 615 Name: strings.Repeat("n", 2000), 616 Namespace: "kittensandponies", 617 } 618 bobRidiculousAttr := authorizer.AttributesRecord{ 619 User: &user.DefaultInfo{Name: "bob"}, 620 ResourceRequest: true, 621 Verb: strings.Repeat("v", 2000), 622 APIGroup: strings.Repeat("g", 2000), 623 APIVersion: strings.Repeat("a", 2000), 624 Resource: strings.Repeat("r", 2000), 625 Name: strings.Repeat("n", 2000), 626 Namespace: "kittensandponies", 627 } 628 629 type webhookCacheTestCase struct { 630 name string 631 632 attr authorizer.AttributesRecord 633 634 allow bool 635 statusCode int 636 637 expectedErr bool 638 expectedAuthorized bool 639 expectedCalls int 640 } 641 642 tests := []webhookCacheTestCase{ 643 // server error and 429's retry 644 {name: "server errors retry", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, 645 {name: "429s retry", attr: aliceAttr, allow: false, statusCode: 429, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, 646 // regular errors return errors but do not retry 647 {name: "404 doesnt retry", attr: aliceAttr, allow: false, statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, 648 {name: "403 doesnt retry", attr: aliceAttr, allow: false, statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, 649 {name: "401 doesnt retry", attr: aliceAttr, allow: false, statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, 650 // successful responses are cached 651 {name: "alice successful request", attr: aliceAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1}, 652 // later requests within the cache window don't hit the backend 653 {name: "alice cached request", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCalls: 0}, 654 655 // a request with different attributes doesn't hit the cache 656 {name: "bob failed request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, 657 // successful response for other attributes is cached 658 {name: "bob unauthorized request", attr: bobAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1}, 659 // later requests within the cache window don't hit the backend 660 {name: "bob unauthorized cached request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: false, expectedCalls: 0}, 661 // ridiculous unauthorized requests are not cached. 662 {name: "ridiculous unauthorized request", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1}, 663 // later ridiculous requests within the cache window still hit the backend 664 {name: "ridiculous unauthorized request again", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1}, 665 // ridiculous authorized requests are not cached. 666 {name: "ridiculous authorized request", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1}, 667 // later ridiculous requests within the cache window still hit the backend 668 {name: "ridiculous authorized request again", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1}, 669 } 670 671 for i, test := range tests { 672 t.Run(test.name, func(t *testing.T) { 673 serv.called = 0 674 serv.allow = test.allow 675 serv.statusCode = test.statusCode 676 authorized, _, err := wh.Authorize(context.Background(), test.attr) 677 if test.expectedErr && err == nil { 678 t.Fatalf("%d: Expected error", i) 679 } else if !test.expectedErr && err != nil { 680 t.Fatalf("%d: unexpected error: %v", i, err) 681 } 682 683 if test.expectedAuthorized != (authorized == authorizer.DecisionAllow) { 684 t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedAuthorized, authorized) 685 } 686 687 if test.expectedCalls != serv.called { 688 t.Errorf("%d: expected %d calls, got %d", i, test.expectedCalls, serv.called) 689 } 690 }) 691 } 692 } 693 694 // TestStructuredAuthzConfigFeatureEnablement verifies cel expressions can only be used when feature is enabled 695 func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) { 696 697 service := new(mockV1Service) 698 service.statusCode = 200 699 service.Allow() 700 s, err := NewV1TestServer(service, serverCert, serverKey, caCert) 701 if err != nil { 702 t.Fatal(err) 703 } 704 defer s.Close() 705 706 labelRequirement, _ := labels.NewRequirement("baz", selection.Equals, []string{"qux"}) 707 708 type webhookMatchConditionsTestCase struct { 709 name string 710 attr authorizer.AttributesRecord 711 allow bool 712 expectedCompileErr bool 713 expectedEvalErr bool 714 expectedDecision authorizer.Decision 715 expressions []apiserver.WebhookMatchCondition 716 featureEnabled bool 717 selectorEnabled bool 718 } 719 aliceAttr := authorizer.AttributesRecord{ 720 User: &user.DefaultInfo{ 721 Name: "alice", 722 UID: "1", 723 Groups: []string{"group1", "group2"}, 724 Extra: map[string][]string{"key1": {"a", "b", "c"}}, 725 }, 726 ResourceRequest: true, 727 Namespace: "kittensandponies", 728 Verb: "get", 729 } 730 aliceWithSelectorsAttr := authorizer.AttributesRecord{ 731 User: &user.DefaultInfo{ 732 Name: "alice", 733 UID: "1", 734 Groups: []string{"group1", "group2"}, 735 Extra: map[string][]string{"key1": {"a", "b", "c"}}, 736 }, 737 ResourceRequest: true, 738 Namespace: "kittensandponies", 739 Verb: "get", 740 FieldSelectorRequirements: fields.Requirements{fields.Requirement{Field: "foo", Operator: selection.Equals, Value: "bar"}}, 741 LabelSelectorRequirements: labels.Requirements{*labelRequirement}, 742 } 743 tests := []webhookMatchConditionsTestCase{ 744 { 745 name: "no match condition does not require feature enablement", 746 attr: aliceAttr, 747 allow: true, 748 expectedCompileErr: false, 749 expectedDecision: authorizer.DecisionAllow, 750 expressions: []apiserver.WebhookMatchCondition{}, 751 featureEnabled: false, 752 }, 753 { 754 name: "should fail when match conditions are used without feature enabled", 755 attr: aliceAttr, 756 allow: false, 757 expectedCompileErr: true, 758 expectedDecision: authorizer.DecisionNoOpinion, 759 expressions: []apiserver.WebhookMatchCondition{ 760 { 761 Expression: "request.user == 'alice'", 762 }, 763 }, 764 featureEnabled: false, 765 }, 766 { 767 name: "feature enabled, match all against all expressions", 768 attr: aliceWithSelectorsAttr, 769 allow: true, 770 expectedCompileErr: false, 771 expectedDecision: authorizer.DecisionAllow, 772 expressions: []apiserver.WebhookMatchCondition{ 773 { 774 Expression: "request.user == 'alice'", 775 }, 776 { 777 Expression: "request.uid == '1'", 778 }, 779 { 780 Expression: "('group1' in request.groups)", 781 }, 782 { 783 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", 784 }, 785 { 786 Expression: "request.?resourceAttributes.fieldSelector.requirements.orValue([]).exists(r, r.key=='foo' && r.operator=='In' && ('bar' in r.values))", 787 }, 788 { 789 Expression: "request.?resourceAttributes.labelSelector.requirements.orValue([]).exists(r, r.key=='baz' && r.operator=='In' && ('qux' in r.values))", 790 }, 791 { 792 Expression: "request.resourceAttributes.?labelSelector.requirements.orValue([]).exists(r, r.key=='baz' && r.operator=='In' && ('qux' in r.values))", 793 }, 794 { 795 Expression: "request.resourceAttributes.labelSelector.?requirements.orValue([]).exists(r, r.key=='baz' && r.operator=='In' && ('qux' in r.values))", 796 }, 797 }, 798 featureEnabled: true, 799 selectorEnabled: true, 800 }, 801 } 802 803 for i, test := range tests { 804 t.Run(test.name, func(t *testing.T) { 805 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, test.featureEnabled) 806 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthorizeWithSelectors, test.selectorEnabled) 807 wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions, "") 808 if test.expectedCompileErr && err == nil { 809 t.Fatalf("%d: Expected compile error", i) 810 } else if !test.expectedCompileErr && err != nil { 811 t.Fatalf("%d: unexpected error when creating a new WebhookAuthorizer: %v", i, err) 812 } 813 if err == nil { 814 authorized, _, err := wh.Authorize(context.Background(), test.attr) 815 if test.expectedEvalErr && err == nil { 816 t.Fatalf("%d: Expected eval error", i) 817 } else if !test.expectedEvalErr && err != nil { 818 t.Fatalf("%d: unexpected error when authorizing: %v", i, err) 819 } 820 821 if test.expectedDecision != authorized { 822 t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedDecision, authorized) 823 } 824 } 825 }) 826 } 827 } 828 829 func TestWebhookMetrics(t *testing.T) { 830 service := new(mockV1Service) 831 service.statusCode = 200 832 service.Allow() 833 s, err := NewV1TestServer(service, serverCert, serverKey, caCert) 834 if err != nil { 835 t.Fatal(err) 836 } 837 defer s.Close() 838 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true) 839 840 aliceAttr := authorizer.AttributesRecord{ 841 User: &user.DefaultInfo{ 842 Name: "alice", 843 UID: "1", 844 }, 845 } 846 847 testCases := []struct { 848 name string 849 attr authorizer.AttributesRecord 850 expressions1 []apiserver.WebhookMatchCondition 851 expressions2 []apiserver.WebhookMatchCondition 852 metrics []string 853 want string 854 }{ 855 { 856 name: "should have one evaluation error from multiple failed match conditions", 857 attr: aliceAttr, 858 expressions1: []apiserver.WebhookMatchCondition{ 859 { 860 Expression: "request.user == 'alice'", 861 }, 862 { 863 Expression: "request.resourceAttributes.verb == 'get'", 864 }, 865 { 866 Expression: "request.resourceAttributes.namespace == 'kittensandponies'", 867 }, 868 }, 869 expressions2: []apiserver.WebhookMatchCondition{ 870 { 871 Expression: "request.user == 'alice'", 872 }, 873 }, 874 metrics: []string{ 875 "apiserver_authorization_match_condition_evaluation_errors_total", 876 }, 877 want: fmt.Sprintf(` 878 # HELP apiserver_authorization_match_condition_evaluation_errors_total [ALPHA] Total number of errors when an authorization webhook encounters a match condition error split by authorizer type and name. 879 # TYPE apiserver_authorization_match_condition_evaluation_errors_total counter 880 apiserver_authorization_match_condition_evaluation_errors_total{name="%s",type="%s"} 1 881 `, "wh1.example.com", "Webhook"), 882 }, 883 { 884 name: "should have two webhook exclusions due to match condition", 885 attr: aliceAttr, 886 expressions1: []apiserver.WebhookMatchCondition{ 887 { 888 Expression: "request.user == 'alice2'", 889 }, 890 { 891 Expression: "request.uid == '1'", 892 }, 893 }, 894 expressions2: []apiserver.WebhookMatchCondition{ 895 { 896 Expression: "request.user == 'alice1'", 897 }, 898 }, 899 metrics: []string{ 900 "apiserver_authorization_match_condition_exclusions_total", 901 }, 902 want: fmt.Sprintf(` 903 # HELP apiserver_authorization_match_condition_exclusions_total [ALPHA] Total number of exclusions when an authorization webhook is skipped because match conditions exclude it. 904 # TYPE apiserver_authorization_match_condition_exclusions_total counter 905 apiserver_authorization_match_condition_exclusions_total{name="%s",type="%s"} 1 906 apiserver_authorization_match_condition_exclusions_total{name="%s",type="%s"} 1 907 `, "wh1.example.com", "Webhook", "wh2.example.com", "Webhook"), 908 }, 909 } 910 911 for _, tt := range testCases { 912 t.Run(tt.name, func(t *testing.T) { 913 celmetrics.ResetMetricsForTest() 914 defer celmetrics.ResetMetricsForTest() 915 wh1, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), tt.expressions1, "wh1.example.com") 916 if err != nil { 917 t.Fatal(err) 918 } 919 wh2, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), tt.expressions2, "wh2.example.com") 920 if err != nil { 921 t.Fatal(err) 922 } 923 if err == nil { 924 _, _, _ = wh1.Authorize(context.Background(), tt.attr) 925 _, _, _ = wh2.Authorize(context.Background(), tt.attr) 926 } 927 928 if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil { 929 t.Fatal(err) 930 } 931 }) 932 } 933 } 934 935 func BenchmarkNoCELExpressionFeatureOff(b *testing.B) { 936 expressions := []apiserver.WebhookMatchCondition{} 937 b.Run("compile", func(b *testing.B) { 938 benchmarkNewWebhookAuthorizer(b, expressions, false) 939 }) 940 b.Run("authorize", func(b *testing.B) { 941 benchmarkWebhookAuthorize(b, expressions, false) 942 }) 943 } 944 945 func BenchmarkNoCELExpressionFeatureOn(b *testing.B) { 946 expressions := []apiserver.WebhookMatchCondition{} 947 b.Run("compile", func(b *testing.B) { 948 benchmarkNewWebhookAuthorizer(b, expressions, true) 949 }) 950 b.Run("authorize", func(b *testing.B) { 951 benchmarkWebhookAuthorize(b, expressions, true) 952 }) 953 } 954 func BenchmarkWithOneCELExpressions(b *testing.B) { 955 expressions := []apiserver.WebhookMatchCondition{ 956 { 957 Expression: "request.user == 'alice'", 958 }, 959 } 960 b.Run("compile", func(b *testing.B) { 961 benchmarkNewWebhookAuthorizer(b, expressions, true) 962 }) 963 b.Run("authorize", func(b *testing.B) { 964 benchmarkWebhookAuthorize(b, expressions, true) 965 }) 966 } 967 func BenchmarkWithOneCELExpressionsFalse(b *testing.B) { 968 expressions := []apiserver.WebhookMatchCondition{ 969 { 970 Expression: "request.user == 'alice2'", 971 }, 972 } 973 b.Run("compile", func(b *testing.B) { 974 benchmarkNewWebhookAuthorizer(b, expressions, true) 975 }) 976 b.Run("authorize", func(b *testing.B) { 977 benchmarkWebhookAuthorize(b, expressions, true) 978 }) 979 } 980 func BenchmarkWithTwoCELExpressions(b *testing.B) { 981 expressions := []apiserver.WebhookMatchCondition{ 982 { 983 Expression: "request.user == 'alice'", 984 }, 985 { 986 Expression: "request.uid == '1'", 987 }, 988 } 989 b.Run("compile", func(b *testing.B) { 990 benchmarkNewWebhookAuthorizer(b, expressions, true) 991 }) 992 b.Run("authorize", func(b *testing.B) { 993 benchmarkWebhookAuthorize(b, expressions, true) 994 }) 995 } 996 func BenchmarkWithTwoCELExpressionsFalse(b *testing.B) { 997 expressions := []apiserver.WebhookMatchCondition{ 998 { 999 Expression: "request.user == 'alice'", 1000 }, 1001 { 1002 Expression: "request.uid == '2'", 1003 }, 1004 } 1005 b.Run("compile", func(b *testing.B) { 1006 benchmarkNewWebhookAuthorizer(b, expressions, true) 1007 }) 1008 b.Run("authorize", func(b *testing.B) { 1009 benchmarkWebhookAuthorize(b, expressions, true) 1010 }) 1011 } 1012 func BenchmarkWithManyCELExpressions(b *testing.B) { 1013 expressions := []apiserver.WebhookMatchCondition{ 1014 { 1015 Expression: "request.user == 'alice'", 1016 }, 1017 { 1018 Expression: "request.uid == '1'", 1019 }, 1020 { 1021 Expression: "('group1' in request.groups)", 1022 }, 1023 { 1024 Expression: "('key1' in request.extra)", 1025 }, 1026 { 1027 Expression: "!('key2' in request.extra)", 1028 }, 1029 { 1030 Expression: "('a' in request.extra['key1'])", 1031 }, 1032 { 1033 Expression: "!('z' in request.extra['key1'])", 1034 }, 1035 { 1036 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", 1037 }, 1038 } 1039 b.Run("compile", func(b *testing.B) { 1040 benchmarkNewWebhookAuthorizer(b, expressions, true) 1041 }) 1042 b.Run("authorize", func(b *testing.B) { 1043 benchmarkWebhookAuthorize(b, expressions, true) 1044 }) 1045 } 1046 func BenchmarkWithManyCELExpressionsFalse(b *testing.B) { 1047 expressions := []apiserver.WebhookMatchCondition{ 1048 { 1049 Expression: "request.user == 'alice'", 1050 }, 1051 { 1052 Expression: "request.uid == '1'", 1053 }, 1054 { 1055 Expression: "('group1' in request.groups)", 1056 }, 1057 { 1058 Expression: "('key1' in request.extra)", 1059 }, 1060 { 1061 Expression: "!('key2' in request.extra)", 1062 }, 1063 { 1064 Expression: "('a' in request.extra['key1'])", 1065 }, 1066 { 1067 Expression: "!('z' in request.extra['key1'])", 1068 }, 1069 { 1070 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies1'", 1071 }, 1072 } 1073 b.Run("compile", func(b *testing.B) { 1074 benchmarkNewWebhookAuthorizer(b, expressions, true) 1075 }) 1076 b.Run("authorize", func(b *testing.B) { 1077 benchmarkWebhookAuthorize(b, expressions, true) 1078 }) 1079 } 1080 1081 func benchmarkNewWebhookAuthorizer(b *testing.B, expressions []apiserver.WebhookMatchCondition, featureEnabled bool) { 1082 service := new(mockV1Service) 1083 service.statusCode = 200 1084 service.Allow() 1085 s, err := NewV1TestServer(service, serverCert, serverKey, caCert) 1086 if err != nil { 1087 b.Fatal(err) 1088 } 1089 defer s.Close() 1090 featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, featureEnabled) 1091 1092 b.ResetTimer() 1093 for i := 0; i < b.N; i++ { 1094 // Create an authorizer with or without expressions to compile 1095 _, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), expressions, "") 1096 if err != nil { 1097 b.Fatal(err) 1098 } 1099 } 1100 b.StopTimer() 1101 } 1102 1103 func benchmarkWebhookAuthorize(b *testing.B, expressions []apiserver.WebhookMatchCondition, featureEnabled bool) { 1104 attr := authorizer.AttributesRecord{ 1105 User: &user.DefaultInfo{ 1106 Name: "alice", 1107 UID: "1", 1108 Groups: []string{"group1", "group2"}, 1109 Extra: map[string][]string{"key1": {"a", "b", "c"}}, 1110 }, 1111 ResourceRequest: true, 1112 Namespace: "kittensandponies", 1113 Verb: "get", 1114 } 1115 service := new(mockV1Service) 1116 service.statusCode = 200 1117 service.Allow() 1118 s, err := NewV1TestServer(service, serverCert, serverKey, caCert) 1119 if err != nil { 1120 b.Fatal(err) 1121 } 1122 defer s.Close() 1123 featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, featureEnabled) 1124 // Create an authorizer with or without expressions to compile 1125 wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), expressions, "") 1126 if err != nil { 1127 b.Fatal(err) 1128 } 1129 1130 b.ResetTimer() 1131 for i := 0; i < b.N; i++ { 1132 // Call authorize may or may not require cel evaluations 1133 _, _, err = wh.Authorize(context.Background(), attr) 1134 if err != nil { 1135 b.Fatal(err) 1136 } 1137 } 1138 b.StopTimer() 1139 } 1140 1141 // TestV1WebhookMatchConditions verifies cel expressions are compiled and evaluated correctly 1142 func TestV1WebhookMatchConditions(t *testing.T) { 1143 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true) 1144 service := new(mockV1Service) 1145 service.statusCode = 200 1146 service.Allow() 1147 s, err := NewV1TestServer(service, serverCert, serverKey, caCert) 1148 if err != nil { 1149 t.Fatal(err) 1150 } 1151 defer s.Close() 1152 1153 aliceAttr := authorizer.AttributesRecord{ 1154 User: &user.DefaultInfo{ 1155 Name: "alice", 1156 UID: "1", 1157 Groups: []string{"group1", "group2"}, 1158 Extra: map[string][]string{"key1": {"a", "b", "c"}}, 1159 }, 1160 ResourceRequest: true, 1161 Namespace: "kittensandponies", 1162 Verb: "get", 1163 } 1164 bobAttr := authorizer.AttributesRecord{ 1165 User: &user.DefaultInfo{ 1166 Name: "bob", 1167 }, 1168 ResourceRequest: false, 1169 Namespace: "kittensandponies", 1170 Verb: "get", 1171 } 1172 alice2Attr := authorizer.AttributesRecord{ 1173 User: &user.DefaultInfo{ 1174 Name: "alice2", 1175 }, 1176 } 1177 type webhookMatchConditionsTestCase struct { 1178 name string 1179 attr authorizer.AttributesRecord 1180 expectedCompileErr string 1181 expectedEvalErr string 1182 expectedDecision authorizer.Decision 1183 expressions []apiserver.WebhookMatchCondition 1184 } 1185 1186 tests := []webhookMatchConditionsTestCase{ 1187 { 1188 name: "match all with no expressions", 1189 attr: aliceAttr, 1190 expectedCompileErr: "", 1191 expectedDecision: authorizer.DecisionAllow, 1192 expressions: []apiserver.WebhookMatchCondition{}, 1193 }, 1194 { 1195 name: "match all against all expressions", 1196 attr: aliceAttr, 1197 expectedCompileErr: "", 1198 expectedDecision: authorizer.DecisionAllow, 1199 expressions: []apiserver.WebhookMatchCondition{ 1200 { 1201 Expression: "request.user == 'alice'", 1202 }, 1203 { 1204 Expression: "request.uid == '1'", 1205 }, 1206 { 1207 Expression: "('group1' in request.groups)", 1208 }, 1209 { 1210 Expression: "('key1' in request.extra)", 1211 }, 1212 { 1213 Expression: "!('key2' in request.extra)", 1214 }, 1215 { 1216 Expression: "('a' in request.extra['key1'])", 1217 }, 1218 { 1219 Expression: "!('z' in request.extra['key1'])", 1220 }, 1221 { 1222 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", 1223 }, 1224 }, 1225 }, 1226 { 1227 name: "match all except group, eval to one successful false, no error", 1228 attr: aliceAttr, 1229 expectedCompileErr: "", 1230 expectedDecision: authorizer.DecisionNoOpinion, 1231 expectedEvalErr: "", 1232 expressions: []apiserver.WebhookMatchCondition{ 1233 { 1234 Expression: "request.user == 'alice'", 1235 }, 1236 { 1237 Expression: "request.uid == '1'", 1238 }, 1239 { 1240 Expression: "('group3' in request.groups)", 1241 }, 1242 { 1243 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", 1244 }, 1245 }, 1246 }, 1247 { 1248 name: "match condition with one compilation error", 1249 attr: aliceAttr, 1250 expectedCompileErr: "matchConditions[2].expression: Invalid value: \"('group3' in request.group)\": compilation failed: ERROR: <input>:1:21: undefined field 'group'\n | ('group3' in request.group)\n | ....................^", 1251 expectedDecision: authorizer.DecisionNoOpinion, 1252 expressions: []apiserver.WebhookMatchCondition{ 1253 { 1254 Expression: "request.user == 'alice'", 1255 }, 1256 { 1257 Expression: "request.uid == '1'", 1258 }, 1259 { 1260 Expression: "('group3' in request.group)", 1261 }, 1262 { 1263 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", 1264 }, 1265 }, 1266 }, 1267 { 1268 name: "match all except uid", 1269 attr: aliceAttr, 1270 expectedCompileErr: "", 1271 expectedDecision: authorizer.DecisionNoOpinion, 1272 expressions: []apiserver.WebhookMatchCondition{ 1273 { 1274 Expression: "request.user == 'alice'", 1275 }, 1276 { 1277 Expression: "request.uid == '2'", 1278 }, 1279 { 1280 Expression: "('group1' in request.groups)", 1281 }, 1282 { 1283 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", 1284 }, 1285 }, 1286 }, 1287 { 1288 name: "match on user name but not namespace", 1289 attr: aliceAttr, 1290 expectedCompileErr: "", 1291 expectedDecision: authorizer.DecisionNoOpinion, 1292 expressions: []apiserver.WebhookMatchCondition{ 1293 { 1294 Expression: "request.user == 'alice'", 1295 }, 1296 { 1297 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'", 1298 }, 1299 }, 1300 }, 1301 { 1302 name: "mismatch on user name", 1303 attr: bobAttr, 1304 expectedCompileErr: "", 1305 expectedDecision: authorizer.DecisionNoOpinion, 1306 expressions: []apiserver.WebhookMatchCondition{ 1307 { 1308 Expression: "request.user == 'alice'", 1309 }, 1310 }, 1311 }, 1312 { 1313 name: "match on user name but not resourceAttributes", 1314 attr: bobAttr, 1315 expectedCompileErr: "", 1316 expectedDecision: authorizer.DecisionNoOpinion, 1317 expressions: []apiserver.WebhookMatchCondition{ 1318 { 1319 Expression: "request.user == 'bob'", 1320 }, 1321 { 1322 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'", 1323 }, 1324 }, 1325 }, 1326 { 1327 name: "expression failed to compile due to wrong return type", 1328 attr: bobAttr, 1329 expectedCompileErr: `matchConditions[0].expression: Invalid value: "request.user": must evaluate to bool but got string`, 1330 expectedDecision: authorizer.DecisionNoOpinion, 1331 expressions: []apiserver.WebhookMatchCondition{ 1332 { 1333 Expression: "request.user", 1334 }, 1335 }, 1336 }, 1337 { 1338 name: "eval failed due to errors, no successful fail", 1339 attr: alice2Attr, 1340 expectedCompileErr: "", 1341 expectedEvalErr: "cel evaluation error: expression 'request.resourceAttributes.namespace == 'kittensandponies'' resulted in error: no such key: resourceAttributes", 1342 expectedDecision: authorizer.DecisionNoOpinion, 1343 expressions: []apiserver.WebhookMatchCondition{ 1344 { 1345 Expression: "request.user == 'alice2'", 1346 }, 1347 { 1348 Expression: "request.resourceAttributes.namespace == 'kittensandponies'", 1349 }, 1350 }, 1351 }, 1352 { 1353 name: "at least one matchCondition successfully evaluates to FALSE, error ignored", 1354 attr: alice2Attr, 1355 expectedCompileErr: "", 1356 expectedEvalErr: "", 1357 expectedDecision: authorizer.DecisionNoOpinion, 1358 expressions: []apiserver.WebhookMatchCondition{ 1359 { 1360 Expression: "request.user != 'alice2'", 1361 }, 1362 { 1363 Expression: "request.resourceAttributes.namespace == 'kittensandponies'", 1364 }, 1365 }, 1366 }, 1367 { 1368 name: "match on user name but failed to compile due to type check in nonResourceAttributes", 1369 attr: bobAttr, 1370 expectedCompileErr: "matchConditions[1].expression: Invalid value: \"request.nonResourceAttributes.verb == 2\": compilation failed: ERROR: <input>:1:36: found no matching overload for '_==_' applied to '(string, int)'\n | request.nonResourceAttributes.verb == 2\n | ...................................^", 1371 expectedDecision: authorizer.DecisionNoOpinion, 1372 expressions: []apiserver.WebhookMatchCondition{ 1373 { 1374 Expression: "request.user == 'bob'", 1375 }, 1376 { 1377 Expression: "request.nonResourceAttributes.verb == 2", 1378 }, 1379 }, 1380 }, 1381 { 1382 name: "match on user name and nonresourceAttributes", 1383 attr: bobAttr, 1384 expectedCompileErr: "", 1385 expectedDecision: authorizer.DecisionAllow, 1386 expressions: []apiserver.WebhookMatchCondition{ 1387 { 1388 Expression: "request.user == 'bob'", 1389 }, 1390 { 1391 Expression: "has(request.nonResourceAttributes) && request.nonResourceAttributes.verb == 'get'", 1392 }, 1393 }, 1394 }, 1395 { 1396 name: "match eval failed with bad SubjectAccessReviewSpec", 1397 attr: authorizer.AttributesRecord{}, 1398 expectedCompileErr: "", 1399 // default decisionOnError in newWithBackoff to skip 1400 expectedDecision: authorizer.DecisionNoOpinion, 1401 expectedEvalErr: "cel evaluation error: expression 'request.resourceAttributes.verb == 'get'' resulted in error: no such key: resourceAttributes", 1402 expressions: []apiserver.WebhookMatchCondition{ 1403 { 1404 Expression: "request.resourceAttributes.verb == 'get'", 1405 }, 1406 }, 1407 }, 1408 } 1409 1410 for i, test := range tests { 1411 t.Run(test.name, func(t *testing.T) { 1412 wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions, "") 1413 if len(test.expectedCompileErr) > 0 && err == nil { 1414 t.Fatalf("%d: Expected compile error", i) 1415 } else if len(test.expectedCompileErr) == 0 && err != nil { 1416 t.Fatalf("%d: unexpected error when creating a new WebhookAuthorizer: %v", i, err) 1417 } 1418 if err != nil { 1419 if d := cmp.Diff(test.expectedCompileErr, err.Error()); d != "" { 1420 t.Fatalf("newV1Authorizer mismatch (-want +got):\n%s", d) 1421 } 1422 } 1423 if err == nil { 1424 authorized, _, err := wh.Authorize(context.Background(), test.attr) 1425 if len(test.expectedEvalErr) > 0 && err == nil { 1426 t.Fatalf("%d: Expected eval error", i) 1427 } else if len(test.expectedEvalErr) == 0 && err != nil { 1428 t.Fatalf("%d: unexpected error when authorizing: %v", i, err) 1429 } 1430 1431 if err != nil { 1432 if d := cmp.Diff(test.expectedEvalErr, err.Error()); d != "" { 1433 t.Fatalf("Authorize mismatch (-want +got):\n%s", d) 1434 } 1435 } 1436 1437 if test.expectedDecision != authorized { 1438 t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedDecision, authorized) 1439 } 1440 } 1441 }) 1442 } 1443 } 1444 1445 func noopAuthorizerMetrics() metrics.AuthorizerMetrics { 1446 return metrics.NoopAuthorizerMetrics{} 1447 } 1448 1449 func celAuthorizerMetrics() metrics.AuthorizerMetrics { 1450 return celAuthorizerMetricsType{ 1451 MatcherMetrics: celmetrics.NewMatcherMetrics(), 1452 } 1453 } 1454 1455 type celAuthorizerMetricsType struct { 1456 metrics.NoopRequestMetrics 1457 metrics.NoopWebhookMetrics 1458 celmetrics.MatcherMetrics 1459 }