k8s.io/apiserver@v0.31.1/plugin/pkg/authenticator/token/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 "reflect" 31 "testing" 32 "time" 33 34 authenticationv1beta1 "k8s.io/api/authentication/v1beta1" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apiserver/pkg/authentication/authenticator" 37 "k8s.io/apiserver/pkg/authentication/token/cache" 38 "k8s.io/apiserver/pkg/authentication/user" 39 webhookutil "k8s.io/apiserver/pkg/util/webhook" 40 v1 "k8s.io/client-go/tools/clientcmd/api/v1" 41 ) 42 43 var apiAuds = authenticator.Audiences{"api"} 44 45 // V1beta1Service mocks a remote authentication service. 46 type V1beta1Service interface { 47 // Review looks at the TokenReviewSpec and provides an authentication 48 // response in the TokenReviewStatus. 49 Review(*authenticationv1beta1.TokenReview) 50 HTTPStatusCode() int 51 } 52 53 // NewV1beta1TestServer wraps a V1beta1Service as an httptest.Server. 54 func NewV1beta1TestServer(s V1beta1Service, cert, key, caCert []byte) (*httptest.Server, error) { 55 const webhookPath = "/testserver" 56 var tlsConfig *tls.Config 57 if cert != nil { 58 cert, err := tls.X509KeyPair(cert, key) 59 if err != nil { 60 return nil, err 61 } 62 tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} 63 } 64 65 if caCert != nil { 66 rootCAs := x509.NewCertPool() 67 rootCAs.AppendCertsFromPEM(caCert) 68 if tlsConfig == nil { 69 tlsConfig = &tls.Config{} 70 } 71 tlsConfig.ClientCAs = rootCAs 72 tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 73 } 74 75 serveHTTP := func(w http.ResponseWriter, r *http.Request) { 76 if r.Method != "POST" { 77 http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed) 78 return 79 } 80 if r.URL.Path != webhookPath { 81 http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound) 82 return 83 } 84 85 var review authenticationv1beta1.TokenReview 86 bodyData, _ := ioutil.ReadAll(r.Body) 87 if err := json.Unmarshal(bodyData, &review); err != nil { 88 http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest) 89 return 90 } 91 // ensure we received the serialized tokenreview as expected 92 if review.APIVersion != "authentication.k8s.io/v1beta1" { 93 http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest) 94 return 95 } 96 // once we have a successful request, always call the review to record that we were called 97 s.Review(&review) 98 if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 { 99 http.Error(w, "HTTP Error", s.HTTPStatusCode()) 100 return 101 } 102 type userInfo struct { 103 Username string `json:"username"` 104 UID string `json:"uid"` 105 Groups []string `json:"groups"` 106 Extra map[string][]string `json:"extra"` 107 } 108 type status struct { 109 Authenticated bool `json:"authenticated"` 110 User userInfo `json:"user"` 111 Audiences []string `json:"audiences"` 112 } 113 114 var extra map[string][]string 115 if review.Status.User.Extra != nil { 116 extra = map[string][]string{} 117 for k, v := range review.Status.User.Extra { 118 extra[k] = v 119 } 120 } 121 122 resp := struct { 123 Kind string `json:"kind"` 124 APIVersion string `json:"apiVersion"` 125 Status status `json:"status"` 126 }{ 127 Kind: "TokenReview", 128 APIVersion: authenticationv1beta1.SchemeGroupVersion.String(), 129 Status: status{ 130 review.Status.Authenticated, 131 userInfo{ 132 Username: review.Status.User.Username, 133 UID: review.Status.User.UID, 134 Groups: review.Status.User.Groups, 135 Extra: extra, 136 }, 137 review.Status.Audiences, 138 }, 139 } 140 w.Header().Set("Content-Type", "application/json") 141 json.NewEncoder(w).Encode(resp) 142 } 143 144 server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP)) 145 server.TLS = tlsConfig 146 server.StartTLS() 147 148 // Adjust the path to point to our custom path 149 serverURL, _ := url.Parse(server.URL) 150 serverURL.Path = webhookPath 151 server.URL = serverURL.String() 152 153 return server, nil 154 } 155 156 // A service that can be set to say yes or no to authentication requests. 157 type mockV1beta1Service struct { 158 allow bool 159 statusCode int 160 called int 161 } 162 163 func (m *mockV1beta1Service) Review(r *authenticationv1beta1.TokenReview) { 164 m.called++ 165 r.Status.Authenticated = m.allow 166 if m.allow { 167 r.Status.User.Username = "realHooman@email.com" 168 } 169 } 170 func (m *mockV1beta1Service) Allow() { m.allow = true } 171 func (m *mockV1beta1Service) Deny() { m.allow = false } 172 func (m *mockV1beta1Service) HTTPStatusCode() int { return m.statusCode } 173 174 // newV1beta1TokenAuthenticator creates a temporary kubeconfig file from the provided 175 // arguments and attempts to load a new WebhookTokenAuthenticator from it. 176 func newV1beta1TokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) { 177 tempfile, err := ioutil.TempFile("", "") 178 if err != nil { 179 return nil, err 180 } 181 p := tempfile.Name() 182 defer os.Remove(p) 183 config := v1.Config{ 184 Clusters: []v1.NamedCluster{ 185 { 186 Cluster: v1.Cluster{Server: serverURL, CertificateAuthorityData: ca}, 187 }, 188 }, 189 AuthInfos: []v1.NamedAuthInfo{ 190 { 191 AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey}, 192 }, 193 }, 194 } 195 if err := json.NewEncoder(tempfile).Encode(config); err != nil { 196 return nil, err 197 } 198 199 clientConfig, err := webhookutil.LoadKubeconfig(p, nil) 200 if err != nil { 201 return nil, err 202 } 203 204 c, err := tokenReviewInterfaceFromConfig(clientConfig, "v1beta1", testRetryBackoff) 205 if err != nil { 206 return nil, err 207 } 208 209 authn, err := newWithBackoff(c, testRetryBackoff, implicitAuds, 10*time.Second, AuthenticatorMetrics{ 210 RecordRequestTotal: noopMetrics{}.RequestTotal, 211 RecordRequestLatency: noopMetrics{}.RequestLatency, 212 }) 213 if err != nil { 214 return nil, err 215 } 216 217 return cache.New(authn, false, cacheTime, cacheTime), nil 218 } 219 220 func TestV1beta1TLSConfig(t *testing.T) { 221 tests := []struct { 222 test string 223 clientCert, clientKey, clientCA []byte 224 serverCert, serverKey, serverCA []byte 225 wantErr bool 226 }{ 227 { 228 test: "TLS setup between client and server", 229 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 230 serverCert: serverCert, serverKey: serverKey, serverCA: caCert, 231 }, 232 { 233 test: "Server does not require client auth", 234 clientCA: caCert, 235 serverCert: serverCert, serverKey: serverKey, 236 }, 237 { 238 test: "Server does not require client auth, client provides it", 239 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 240 serverCert: serverCert, serverKey: serverKey, 241 }, 242 { 243 test: "Client does not trust server", 244 clientCert: clientCert, clientKey: clientKey, 245 serverCert: serverCert, serverKey: serverKey, 246 wantErr: true, 247 }, 248 { 249 test: "Server does not trust client", 250 clientCert: clientCert, clientKey: clientKey, clientCA: caCert, 251 serverCert: serverCert, serverKey: serverKey, serverCA: badCACert, 252 wantErr: true, 253 }, 254 { 255 // Plugin does not support insecure configurations. 256 test: "Server is using insecure connection", 257 wantErr: true, 258 }, 259 } 260 for _, tt := range tests { 261 // Use a closure so defer statements trigger between loop iterations. 262 func() { 263 service := new(mockV1beta1Service) 264 service.statusCode = 200 265 266 server, err := NewV1beta1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA) 267 if err != nil { 268 t.Errorf("%s: failed to create server: %v", tt.test, err) 269 return 270 } 271 defer server.Close() 272 273 wh, err := newV1beta1TokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil) 274 if err != nil { 275 t.Errorf("%s: failed to create client: %v", tt.test, err) 276 return 277 } 278 279 // Allow all and see if we get an error. 280 service.Allow() 281 _, authenticated, err := wh.AuthenticateToken(context.Background(), "t0k3n") 282 if tt.wantErr { 283 if err == nil { 284 t.Errorf("expected error making authorization request: %v", err) 285 } 286 return 287 } 288 if !authenticated { 289 t.Errorf("%s: failed to authenticate token", tt.test) 290 return 291 } 292 293 service.Deny() 294 _, authenticated, err = wh.AuthenticateToken(context.Background(), "t0k3n") 295 if err != nil { 296 t.Errorf("%s: unexpectedly failed AuthenticateToken", tt.test) 297 } 298 if authenticated { 299 t.Errorf("%s: incorrectly authenticated token", tt.test) 300 } 301 }() 302 } 303 } 304 305 // recorderV1beta1Service records all token review requests, and responds with the 306 // provided TokenReviewStatus. 307 type recorderV1beta1Service struct { 308 lastRequest authenticationv1beta1.TokenReview 309 response authenticationv1beta1.TokenReviewStatus 310 } 311 312 func (rec *recorderV1beta1Service) Review(r *authenticationv1beta1.TokenReview) { 313 rec.lastRequest = *r 314 r.Status = rec.response 315 } 316 317 func (rec *recorderV1beta1Service) HTTPStatusCode() int { return 200 } 318 319 func TestV1beta1WebhookTokenAuthenticator(t *testing.T) { 320 serv := &recorderV1beta1Service{} 321 322 s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert) 323 if err != nil { 324 t.Fatal(err) 325 } 326 defer s.Close() 327 328 expTypeMeta := metav1.TypeMeta{ 329 APIVersion: "authentication.k8s.io/v1beta1", 330 Kind: "TokenReview", 331 } 332 333 tests := []struct { 334 description string 335 implicitAuds, reqAuds authenticator.Audiences 336 serverResponse authenticationv1beta1.TokenReviewStatus 337 expectedAuthenticated bool 338 expectedUser *user.DefaultInfo 339 expectedAuds authenticator.Audiences 340 }{ 341 { 342 description: "successful response should pass through all user info.", 343 serverResponse: authenticationv1beta1.TokenReviewStatus{ 344 Authenticated: true, 345 User: authenticationv1beta1.UserInfo{ 346 Username: "somebody", 347 }, 348 }, 349 expectedAuthenticated: true, 350 expectedUser: &user.DefaultInfo{ 351 Name: "somebody", 352 }, 353 }, 354 { 355 description: "successful response should pass through all user info.", 356 serverResponse: authenticationv1beta1.TokenReviewStatus{ 357 Authenticated: true, 358 User: authenticationv1beta1.UserInfo{ 359 Username: "person@place.com", 360 UID: "abcd-1234", 361 Groups: []string{"stuff-dev", "main-eng"}, 362 Extra: map[string]authenticationv1beta1.ExtraValue{"foo": {"bar", "baz"}}, 363 }, 364 }, 365 expectedAuthenticated: true, 366 expectedUser: &user.DefaultInfo{ 367 Name: "person@place.com", 368 UID: "abcd-1234", 369 Groups: []string{"stuff-dev", "main-eng"}, 370 Extra: map[string][]string{"foo": {"bar", "baz"}}, 371 }, 372 }, 373 { 374 description: "unauthenticated shouldn't even include extra provided info.", 375 serverResponse: authenticationv1beta1.TokenReviewStatus{ 376 Authenticated: false, 377 User: authenticationv1beta1.UserInfo{ 378 Username: "garbage", 379 UID: "abcd-1234", 380 Groups: []string{"not-actually-used"}, 381 }, 382 }, 383 expectedAuthenticated: false, 384 expectedUser: nil, 385 }, 386 { 387 description: "unauthenticated shouldn't even include extra provided info.", 388 serverResponse: authenticationv1beta1.TokenReviewStatus{ 389 Authenticated: false, 390 }, 391 expectedAuthenticated: false, 392 expectedUser: nil, 393 }, 394 { 395 description: "good audience", 396 implicitAuds: apiAuds, 397 reqAuds: apiAuds, 398 serverResponse: authenticationv1beta1.TokenReviewStatus{ 399 Authenticated: true, 400 User: authenticationv1beta1.UserInfo{ 401 Username: "somebody", 402 }, 403 }, 404 expectedAuthenticated: true, 405 expectedUser: &user.DefaultInfo{ 406 Name: "somebody", 407 }, 408 expectedAuds: apiAuds, 409 }, 410 { 411 description: "good audience", 412 implicitAuds: append(apiAuds, "other"), 413 reqAuds: apiAuds, 414 serverResponse: authenticationv1beta1.TokenReviewStatus{ 415 Authenticated: true, 416 User: authenticationv1beta1.UserInfo{ 417 Username: "somebody", 418 }, 419 }, 420 expectedAuthenticated: true, 421 expectedUser: &user.DefaultInfo{ 422 Name: "somebody", 423 }, 424 expectedAuds: apiAuds, 425 }, 426 { 427 description: "bad audiences", 428 implicitAuds: apiAuds, 429 reqAuds: authenticator.Audiences{"other"}, 430 serverResponse: authenticationv1beta1.TokenReviewStatus{ 431 Authenticated: false, 432 }, 433 expectedAuthenticated: false, 434 }, 435 { 436 description: "bad audiences", 437 implicitAuds: apiAuds, 438 reqAuds: authenticator.Audiences{"other"}, 439 // webhook authenticator hasn't been upgraded to support audience. 440 serverResponse: authenticationv1beta1.TokenReviewStatus{ 441 Authenticated: true, 442 User: authenticationv1beta1.UserInfo{ 443 Username: "somebody", 444 }, 445 }, 446 expectedAuthenticated: false, 447 }, 448 { 449 description: "audience aware backend", 450 implicitAuds: apiAuds, 451 reqAuds: apiAuds, 452 serverResponse: authenticationv1beta1.TokenReviewStatus{ 453 Authenticated: true, 454 User: authenticationv1beta1.UserInfo{ 455 Username: "somebody", 456 }, 457 Audiences: []string(apiAuds), 458 }, 459 expectedAuthenticated: true, 460 expectedUser: &user.DefaultInfo{ 461 Name: "somebody", 462 }, 463 expectedAuds: apiAuds, 464 }, 465 { 466 description: "audience aware backend", 467 serverResponse: authenticationv1beta1.TokenReviewStatus{ 468 Authenticated: true, 469 User: authenticationv1beta1.UserInfo{ 470 Username: "somebody", 471 }, 472 Audiences: []string(apiAuds), 473 }, 474 expectedAuthenticated: true, 475 expectedUser: &user.DefaultInfo{ 476 Name: "somebody", 477 }, 478 }, 479 { 480 description: "audience aware backend", 481 implicitAuds: apiAuds, 482 reqAuds: apiAuds, 483 serverResponse: authenticationv1beta1.TokenReviewStatus{ 484 Authenticated: true, 485 User: authenticationv1beta1.UserInfo{ 486 Username: "somebody", 487 }, 488 Audiences: []string{"other"}, 489 }, 490 expectedAuthenticated: false, 491 }, 492 } 493 token := "my-s3cr3t-t0ken" // Fake token for testing. 494 for _, tt := range tests { 495 t.Run(tt.description, func(t *testing.T) { 496 wh, err := newV1beta1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds) 497 if err != nil { 498 t.Fatal(err) 499 } 500 501 ctx := context.Background() 502 if tt.reqAuds != nil { 503 ctx = authenticator.WithAudiences(ctx, tt.reqAuds) 504 } 505 506 serv.response = tt.serverResponse 507 resp, authenticated, err := wh.AuthenticateToken(ctx, token) 508 if err != nil { 509 t.Fatalf("authentication failed: %v", err) 510 } 511 if serv.lastRequest.Spec.Token != token { 512 t.Errorf("Server did not see correct token. Got %q, expected %q.", 513 serv.lastRequest.Spec.Token, token) 514 } 515 if !reflect.DeepEqual(serv.lastRequest.TypeMeta, expTypeMeta) { 516 t.Errorf("Server did not see correct TypeMeta. Got %v, expected %v", 517 serv.lastRequest.TypeMeta, expTypeMeta) 518 } 519 if authenticated != tt.expectedAuthenticated { 520 t.Errorf("Plugin returned incorrect authentication response. Got %t, expected %t.", 521 authenticated, tt.expectedAuthenticated) 522 } 523 if resp != nil && tt.expectedUser != nil && !reflect.DeepEqual(resp.User, tt.expectedUser) { 524 t.Errorf("Plugin returned incorrect user. Got %#v, expected %#v", 525 resp.User, tt.expectedUser) 526 } 527 if resp != nil && tt.expectedAuds != nil && !reflect.DeepEqual(resp.Audiences, tt.expectedAuds) { 528 t.Errorf("Plugin returned incorrect audiences. Got %#v, expected %#v", 529 resp.Audiences, tt.expectedAuds) 530 } 531 }) 532 } 533 } 534 535 type authenticationV1beta1UserInfo authenticationv1beta1.UserInfo 536 537 func (a *authenticationV1beta1UserInfo) GetName() string { return a.Username } 538 func (a *authenticationV1beta1UserInfo) GetUID() string { return a.UID } 539 func (a *authenticationV1beta1UserInfo) GetGroups() []string { return a.Groups } 540 541 func (a *authenticationV1beta1UserInfo) GetExtra() map[string][]string { 542 if a.Extra == nil { 543 return nil 544 } 545 ret := map[string][]string{} 546 for k, v := range a.Extra { 547 ret[k] = []string(v) 548 } 549 550 return ret 551 } 552 553 // Ensure authenticationv1beta1.UserInfo contains the fields necessary to implement the 554 // user.Info interface. 555 var _ user.Info = (*authenticationV1beta1UserInfo)(nil) 556 557 // TestWebhookCache verifies that error responses from the server are not 558 // cached, but successful responses are. It also ensures that the webhook 559 // call is retried on 429 and 500+ errors 560 func TestV1beta1WebhookCacheAndRetry(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 authenticator that caches successful responses "forever" (100 days). 569 wh, err := newV1beta1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil) 570 if err != nil { 571 t.Fatal(err) 572 } 573 574 testcases := []struct { 575 description string 576 577 token string 578 allow bool 579 code int 580 581 expectError bool 582 expectOk bool 583 expectCalls int 584 }{ 585 { 586 description: "t0k3n, 500 error, retries and fails", 587 588 token: "t0k3n", 589 allow: false, 590 code: 500, 591 592 expectError: true, 593 expectOk: false, 594 expectCalls: 5, 595 }, 596 { 597 description: "t0k3n, 404 error, fails (but no retry)", 598 599 token: "t0k3n", 600 allow: false, 601 code: 404, 602 603 expectError: true, 604 expectOk: false, 605 expectCalls: 1, 606 }, 607 { 608 description: "t0k3n, 200 response, allowed, succeeds with a single call", 609 610 token: "t0k3n", 611 allow: true, 612 code: 200, 613 614 expectError: false, 615 expectOk: true, 616 expectCalls: 1, 617 }, 618 { 619 description: "t0k3n, 500 response, disallowed, but never called because previous 200 response was cached", 620 621 token: "t0k3n", 622 allow: false, 623 code: 500, 624 625 expectError: false, 626 expectOk: true, 627 expectCalls: 0, 628 }, 629 630 { 631 description: "an0th3r_t0k3n, 500 response, disallowed, should be called again with retries", 632 633 token: "an0th3r_t0k3n", 634 allow: false, 635 code: 500, 636 637 expectError: true, 638 expectOk: false, 639 expectCalls: 5, 640 }, 641 { 642 description: "an0th3r_t0k3n, 429 response, disallowed, should be called again with retries", 643 644 token: "an0th3r_t0k3n", 645 allow: false, 646 code: 429, 647 648 expectError: true, 649 expectOk: false, 650 expectCalls: 5, 651 }, 652 { 653 description: "an0th3r_t0k3n, 200 response, allowed, succeeds with a single call", 654 655 token: "an0th3r_t0k3n", 656 allow: true, 657 code: 200, 658 659 expectError: false, 660 expectOk: true, 661 expectCalls: 1, 662 }, 663 { 664 description: "an0th3r_t0k3n, 500 response, disallowed, but never called because previous 200 response was cached", 665 666 token: "an0th3r_t0k3n", 667 allow: false, 668 code: 500, 669 670 expectError: false, 671 expectOk: true, 672 expectCalls: 0, 673 }, 674 } 675 676 for _, testcase := range testcases { 677 t.Run(testcase.description, func(t *testing.T) { 678 serv.allow = testcase.allow 679 serv.statusCode = testcase.code 680 serv.called = 0 681 682 _, ok, err := wh.AuthenticateToken(context.Background(), testcase.token) 683 hasError := err != nil 684 if hasError != testcase.expectError { 685 t.Errorf("Webhook returned HTTP %d, expected error=%v, but got error %v", testcase.code, testcase.expectError, err) 686 } 687 if serv.called != testcase.expectCalls { 688 t.Errorf("Expected %d calls, got %d", testcase.expectCalls, serv.called) 689 } 690 if ok != testcase.expectOk { 691 t.Errorf("Expected ok=%v, got %v", testcase.expectOk, ok) 692 } 693 }) 694 } 695 }