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