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