k8s.io/apiserver@v0.31.1/plugin/pkg/authorizer/webhook/webhook_v1beta1_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 "github.com/google/go-cmp/cmp" 38 39 authorizationv1beta1 "k8s.io/api/authorization/v1beta1" 40 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 41 authzconfig "k8s.io/apiserver/pkg/apis/apiserver" 42 "k8s.io/apiserver/pkg/authentication/user" 43 "k8s.io/apiserver/pkg/authorization/authorizer" 44 webhookutil "k8s.io/apiserver/pkg/util/webhook" 45 v1 "k8s.io/client-go/tools/clientcmd/api/v1" 46 ) 47 48 func TestV1beta1NewFromConfig(t *testing.T) { 49 dir, err := ioutil.TempDir("", "") 50 if err != nil { 51 t.Fatal(err) 52 } 53 defer os.RemoveAll(dir) 54 55 data := struct { 56 CA string 57 Cert string 58 Key string 59 }{ 60 CA: filepath.Join(dir, "ca.pem"), 61 Cert: filepath.Join(dir, "clientcert.pem"), 62 Key: filepath.Join(dir, "clientkey.pem"), 63 } 64 65 files := []struct { 66 name string 67 data []byte 68 }{ 69 {data.CA, caCert}, 70 {data.Cert, clientCert}, 71 {data.Key, clientKey}, 72 } 73 for _, file := range files { 74 if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil { 75 t.Fatal(err) 76 } 77 } 78 79 tests := []struct { 80 msg string 81 configTmpl string 82 wantErr bool 83 }{ 84 { 85 msg: "a single cluster and single user", 86 configTmpl: ` 87 clusters: 88 - cluster: 89 certificate-authority: {{ .CA }} 90 server: https://authz.example.com 91 name: foobar 92 users: 93 - name: a cluster 94 user: 95 client-certificate: {{ .Cert }} 96 client-key: {{ .Key }} 97 `, 98 wantErr: true, 99 }, 100 { 101 msg: "multiple clusters with no context", 102 configTmpl: ` 103 clusters: 104 - cluster: 105 certificate-authority: {{ .CA }} 106 server: https://authz.example.com 107 name: foobar 108 - cluster: 109 certificate-authority: a bad certificate path 110 server: https://authz.example.com 111 name: barfoo 112 users: 113 - name: a name 114 user: 115 client-certificate: {{ .Cert }} 116 client-key: {{ .Key }} 117 `, 118 wantErr: true, 119 }, 120 { 121 msg: "multiple clusters with a context", 122 configTmpl: ` 123 clusters: 124 - cluster: 125 certificate-authority: a bad certificate path 126 server: https://authz.example.com 127 name: foobar 128 - cluster: 129 certificate-authority: {{ .CA }} 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 contexts: 138 - name: default 139 context: 140 cluster: barfoo 141 user: a name 142 current-context: default 143 `, 144 wantErr: false, 145 }, 146 { 147 msg: "cluster with bad certificate path specified", 148 configTmpl: ` 149 clusters: 150 - cluster: 151 certificate-authority: a bad certificate path 152 server: https://authz.example.com 153 name: foobar 154 - cluster: 155 certificate-authority: {{ .CA }} 156 server: https://authz.example.com 157 name: barfoo 158 users: 159 - name: a name 160 user: 161 client-certificate: {{ .Cert }} 162 client-key: {{ .Key }} 163 contexts: 164 - name: default 165 context: 166 cluster: foobar 167 user: a name 168 current-context: default 169 `, 170 wantErr: true, 171 }, 172 } 173 174 for _, tt := range tests { 175 // Use a closure so defer statements trigger between loop iterations. 176 err := func() error { 177 tempfile, err := ioutil.TempFile("", "") 178 if err != nil { 179 return err 180 } 181 p := tempfile.Name() 182 defer os.Remove(p) 183 184 tmpl, err := template.New("test").Parse(tt.configTmpl) 185 if err != nil { 186 return fmt.Errorf("failed to parse test template: %v", err) 187 } 188 if err := tmpl.Execute(tempfile, data); err != nil { 189 return fmt.Errorf("failed to execute test template: %v", err) 190 } 191 // Create a new authorizer 192 clientConfig, err := webhookutil.LoadKubeconfig(p, nil) 193 if err != nil { 194 return err 195 } 196 sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1beta1", testRetryBackoff) 197 if err != nil { 198 return fmt.Errorf("error building sar client: %v", err) 199 } 200 _, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics(), "") 201 return err 202 }() 203 if err != nil && !tt.wantErr { 204 t.Errorf("failed to load plugin from config %q: %v", tt.msg, err) 205 } 206 if err == nil && tt.wantErr { 207 t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg) 208 } 209 } 210 } 211 212 // V1beta1Service mocks a remote service. 213 type V1beta1Service interface { 214 Review(*authorizationv1beta1.SubjectAccessReview) 215 HTTPStatusCode() int 216 } 217 218 // NewV1beta1TestServer wraps a V1beta1Service as an httptest.Server. 219 func NewV1beta1TestServer(s V1beta1Service, cert, key, caCert []byte) (*httptest.Server, error) { 220 const webhookPath = "/testserver" 221 var tlsConfig *tls.Config 222 if cert != nil { 223 cert, err := tls.X509KeyPair(cert, key) 224 if err != nil { 225 return nil, err 226 } 227 tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} 228 } 229 230 if caCert != nil { 231 rootCAs := x509.NewCertPool() 232 rootCAs.AppendCertsFromPEM(caCert) 233 if tlsConfig == nil { 234 tlsConfig = &tls.Config{} 235 } 236 tlsConfig.ClientCAs = rootCAs 237 tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 238 } 239 240 serveHTTP := func(w http.ResponseWriter, r *http.Request) { 241 if r.Method != "POST" { 242 http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed) 243 return 244 } 245 if r.URL.Path != webhookPath { 246 http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound) 247 return 248 } 249 250 var review authorizationv1beta1.SubjectAccessReview 251 bodyData, _ := ioutil.ReadAll(r.Body) 252 if err := json.Unmarshal(bodyData, &review); err != nil { 253 http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest) 254 return 255 } 256 257 // ensure we received the serialized review as expected 258 if review.APIVersion != "authorization.k8s.io/v1beta1" { 259 http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest) 260 return 261 } 262 // once we have a successful request, always call the review to record that we were called 263 s.Review(&review) 264 if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 { 265 http.Error(w, "HTTP Error", s.HTTPStatusCode()) 266 return 267 } 268 type status struct { 269 Allowed bool `json:"allowed"` 270 Reason string `json:"reason"` 271 EvaluationError string `json:"evaluationError"` 272 } 273 resp := struct { 274 APIVersion string `json:"apiVersion"` 275 Status status `json:"status"` 276 }{ 277 APIVersion: authorizationv1beta1.SchemeGroupVersion.String(), 278 Status: status{review.Status.Allowed, review.Status.Reason, review.Status.EvaluationError}, 279 } 280 w.Header().Set("Content-Type", "application/json") 281 json.NewEncoder(w).Encode(resp) 282 } 283 284 server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP)) 285 server.TLS = tlsConfig 286 server.StartTLS() 287 288 // Adjust the path to point to our custom path 289 serverURL, _ := url.Parse(server.URL) 290 serverURL.Path = webhookPath 291 server.URL = serverURL.String() 292 293 return server, nil 294 } 295 296 // A service that can be set to allow all or deny all authorization requests. 297 type mockV1beta1Service struct { 298 allow bool 299 statusCode int 300 called int 301 } 302 303 func (m *mockV1beta1Service) Review(r *authorizationv1beta1.SubjectAccessReview) { 304 m.called++ 305 r.Status.Allowed = m.allow 306 } 307 func (m *mockV1beta1Service) Allow() { m.allow = true } 308 func (m *mockV1beta1Service) Deny() { m.allow = false } 309 func (m *mockV1beta1Service) HTTPStatusCode() int { return m.statusCode } 310 311 // newV1beta1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load 312 // a new WebhookAuthorizer from it. 313 func newV1beta1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration) (*WebhookAuthorizer, error) { 314 tempfile, err := ioutil.TempFile("", "") 315 if err != nil { 316 return nil, err 317 } 318 p := tempfile.Name() 319 defer os.Remove(p) 320 config := v1.Config{ 321 Clusters: []v1.NamedCluster{ 322 { 323 Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca}, 324 }, 325 }, 326 AuthInfos: []v1.NamedAuthInfo{ 327 { 328 AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey}, 329 }, 330 }, 331 } 332 if err := json.NewEncoder(tempfile).Encode(config); err != nil { 333 return nil, err 334 } 335 clientConfig, err := webhookutil.LoadKubeconfig(p, nil) 336 if err != nil { 337 return nil, err 338 } 339 sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1beta1", testRetryBackoff) 340 if err != nil { 341 return nil, fmt.Errorf("error building sar client: %v", err) 342 } 343 return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics(), "") 344 } 345 346 func TestV1beta1TLSConfig(t *testing.T) { 347 tests := []struct { 348 test string 349 clientCert, clientKey, clientCA []byte 350 serverCert, serverKey, serverCA []byte 351 wantAuth, wantErr bool 352 }{ 353 { 354 test: "TLS setup between client and server", 355 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 356 serverCert: serverCert, serverKey: serverKey, serverCA: caCert, 357 wantAuth: true, 358 }, 359 { 360 test: "Server does not require client auth", 361 clientCA: caCert, 362 serverCert: serverCert, serverKey: serverKey, 363 wantAuth: true, 364 }, 365 { 366 test: "Server does not require client auth, client provides it", 367 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 368 serverCert: serverCert, serverKey: serverKey, 369 wantAuth: true, 370 }, 371 { 372 test: "Client does not trust server", 373 clientCert: clientCert, clientKey: clientKey, 374 serverCert: serverCert, serverKey: serverKey, 375 wantErr: true, 376 }, 377 { 378 test: "Server does not trust client", 379 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 380 serverCert: serverCert, serverKey: serverKey, serverCA: badCACert, 381 wantErr: true, 382 }, 383 { 384 // Plugin does not support insecure configurations. 385 test: "Server is using insecure connection", 386 wantErr: true, 387 }, 388 } 389 for _, tt := range tests { 390 // Use a closure so defer statements trigger between loop iterations. 391 func() { 392 service := new(mockV1beta1Service) 393 service.statusCode = 200 394 395 server, err := NewV1beta1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA) 396 if err != nil { 397 t.Errorf("%s: failed to create server: %v", tt.test, err) 398 return 399 } 400 defer server.Close() 401 402 wh, err := newV1beta1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0) 403 if err != nil { 404 t.Errorf("%s: failed to create client: %v", tt.test, err) 405 return 406 } 407 408 attr := authorizer.AttributesRecord{User: &user.DefaultInfo{}} 409 410 // Allow all and see if we get an error. 411 service.Allow() 412 decision, _, err := wh.Authorize(context.Background(), attr) 413 if tt.wantAuth { 414 if decision != authorizer.DecisionAllow { 415 t.Errorf("expected successful authorization") 416 } 417 } else { 418 if decision == authorizer.DecisionAllow { 419 t.Errorf("expected failed authorization") 420 } 421 } 422 if tt.wantErr { 423 if err == nil { 424 t.Errorf("expected error making authorization request: %v", err) 425 } 426 return 427 } 428 if err != nil { 429 t.Errorf("%s: failed to authorize with AllowAll policy: %v", tt.test, err) 430 return 431 } 432 433 service.Deny() 434 if decision, _, _ := wh.Authorize(context.Background(), attr); decision == authorizer.DecisionAllow { 435 t.Errorf("%s: incorrectly authorized with DenyAll policy", tt.test) 436 } 437 }() 438 } 439 } 440 441 // recorderV1beta1Service records all access review requests. 442 type recorderV1beta1Service struct { 443 last authorizationv1beta1.SubjectAccessReview 444 err error 445 } 446 447 func (rec *recorderV1beta1Service) Review(r *authorizationv1beta1.SubjectAccessReview) { 448 rec.last = authorizationv1beta1.SubjectAccessReview{} 449 rec.last = *r 450 r.Status.Allowed = true 451 } 452 453 func (rec *recorderV1beta1Service) Last() (authorizationv1beta1.SubjectAccessReview, error) { 454 return rec.last, rec.err 455 } 456 457 func (rec *recorderV1beta1Service) HTTPStatusCode() int { return 200 } 458 459 func TestV1beta1Webhook(t *testing.T) { 460 serv := new(recorderV1beta1Service) 461 s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert) 462 if err != nil { 463 t.Fatal(err) 464 } 465 defer s.Close() 466 467 wh, err := newV1beta1Authorizer(s.URL, clientCert, clientKey, caCert, 0) 468 if err != nil { 469 t.Fatal(err) 470 } 471 472 expTypeMeta := metav1.TypeMeta{ 473 APIVersion: "authorization.k8s.io/v1beta1", 474 Kind: "SubjectAccessReview", 475 } 476 477 tests := []struct { 478 attr authorizer.Attributes 479 want authorizationv1beta1.SubjectAccessReview 480 }{ 481 { 482 attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}}, 483 want: authorizationv1beta1.SubjectAccessReview{ 484 TypeMeta: expTypeMeta, 485 Spec: authorizationv1beta1.SubjectAccessReviewSpec{ 486 NonResourceAttributes: &authorizationv1beta1.NonResourceAttributes{}, 487 }, 488 }, 489 }, 490 { 491 attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}}, 492 want: authorizationv1beta1.SubjectAccessReview{ 493 TypeMeta: expTypeMeta, 494 Spec: authorizationv1beta1.SubjectAccessReviewSpec{ 495 User: "jane", 496 NonResourceAttributes: &authorizationv1beta1.NonResourceAttributes{}, 497 }, 498 }, 499 }, 500 { 501 attr: authorizer.AttributesRecord{ 502 User: &user.DefaultInfo{ 503 Name: "jane", 504 UID: "1", 505 Groups: []string{"group1", "group2"}, 506 }, 507 Verb: "GET", 508 Namespace: "kittensandponies", 509 APIGroup: "group3", 510 APIVersion: "v7beta3", 511 Resource: "pods", 512 Subresource: "proxy", 513 Name: "my-pod", 514 ResourceRequest: true, 515 Path: "/foo", 516 }, 517 want: authorizationv1beta1.SubjectAccessReview{ 518 TypeMeta: expTypeMeta, 519 Spec: authorizationv1beta1.SubjectAccessReviewSpec{ 520 User: "jane", 521 UID: "1", 522 Groups: []string{"group1", "group2"}, 523 ResourceAttributes: &authorizationv1beta1.ResourceAttributes{ 524 Verb: "GET", 525 Namespace: "kittensandponies", 526 Group: "group3", 527 Version: "v7beta3", 528 Resource: "pods", 529 Subresource: "proxy", 530 Name: "my-pod", 531 }, 532 }, 533 }, 534 }, 535 } 536 537 for i, tt := range tests { 538 decision, _, err := wh.Authorize(context.Background(), tt.attr) 539 if err != nil { 540 t.Fatal(err) 541 } 542 if decision != authorizer.DecisionAllow { 543 t.Errorf("case %d: authorization failed", i) 544 continue 545 } 546 547 gotAttr, err := serv.Last() 548 if err != nil { 549 t.Errorf("case %d: failed to deserialize webhook request: %v", i, err) 550 continue 551 } 552 if !reflect.DeepEqual(gotAttr, tt.want) { 553 t.Errorf("case %d: got != want:\n%s", i, cmp.Diff(gotAttr, tt.want)) 554 } 555 } 556 } 557 558 // TestWebhookCache verifies that error responses from the server are not 559 // cached, but successful responses are. 560 func TestV1beta1WebhookCache(t *testing.T) { 561 serv := new(mockV1beta1Service) 562 s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert) 563 if err != nil { 564 t.Fatal(err) 565 } 566 defer s.Close() 567 568 // Create an authorizer that caches successful responses "forever" (100 days). 569 wh, err := newV1beta1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour) 570 if err != nil { 571 t.Fatal(err) 572 } 573 574 aliceAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "alice"}} 575 bobAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "bob"}} 576 aliceRidiculousAttr := authorizer.AttributesRecord{ 577 User: &user.DefaultInfo{Name: "alice"}, 578 ResourceRequest: true, 579 Verb: strings.Repeat("v", 2000), 580 APIGroup: strings.Repeat("g", 2000), 581 APIVersion: strings.Repeat("a", 2000), 582 Resource: strings.Repeat("r", 2000), 583 Name: strings.Repeat("n", 2000), 584 } 585 bobRidiculousAttr := authorizer.AttributesRecord{ 586 User: &user.DefaultInfo{Name: "bob"}, 587 ResourceRequest: true, 588 Verb: strings.Repeat("v", 2000), 589 APIGroup: strings.Repeat("g", 2000), 590 APIVersion: strings.Repeat("a", 2000), 591 Resource: strings.Repeat("r", 2000), 592 Name: strings.Repeat("n", 2000), 593 } 594 595 type webhookCacheTestCase struct { 596 name string 597 598 attr authorizer.AttributesRecord 599 600 allow bool 601 statusCode int 602 603 expectedErr bool 604 expectedAuthorized bool 605 expectedCalls int 606 } 607 608 tests := []webhookCacheTestCase{ 609 // server error and 429's retry 610 {name: "server errors retry", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, 611 {name: "429s retry", attr: aliceAttr, allow: false, statusCode: 429, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, 612 // regular errors return errors but do not retry 613 {name: "404 doesnt retry", attr: aliceAttr, allow: false, statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, 614 {name: "403 doesnt retry", attr: aliceAttr, allow: false, statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, 615 {name: "401 doesnt retry", attr: aliceAttr, allow: false, statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCalls: 1}, 616 // successful responses are cached 617 {name: "alice successful request", attr: aliceAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1}, 618 // later requests within the cache window don't hit the backend 619 {name: "alice cached request", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCalls: 0}, 620 621 // a request with different attributes doesn't hit the cache 622 {name: "bob failed request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5}, 623 // successful response for other attributes is cached 624 {name: "bob unauthorized request", attr: bobAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1}, 625 // later requests within the cache window don't hit the backend 626 {name: "bob unauthorized cached request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: false, expectedCalls: 0}, 627 // ridiculous unauthorized requests are not cached. 628 {name: "ridiculous unauthorized request", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1}, 629 // later ridiculous requests within the cache window still hit the backend 630 {name: "ridiculous unauthorized request again", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1}, 631 // ridiculous authorized requests are not cached. 632 {name: "ridiculous authorized request", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1}, 633 // later ridiculous requests within the cache window still hit the backend 634 {name: "ridiculous authorized request again", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1}, 635 } 636 637 for i, test := range tests { 638 t.Run(test.name, func(t *testing.T) { 639 serv.called = 0 640 serv.allow = test.allow 641 serv.statusCode = test.statusCode 642 authorized, _, err := wh.Authorize(context.Background(), test.attr) 643 if test.expectedErr && err == nil { 644 t.Fatalf("%d: Expected error", i) 645 } else if !test.expectedErr && err != nil { 646 t.Fatalf("%d: unexpected error: %v", i, err) 647 } 648 649 if test.expectedAuthorized != (authorized == authorizer.DecisionAllow) { 650 t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedAuthorized, authorized) 651 } 652 653 if test.expectedCalls != serv.called { 654 t.Errorf("%d: expected %d calls, got %d", i, test.expectedCalls, serv.called) 655 } 656 }) 657 } 658 }