1/* 2Copyright 2018 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 exec 18 19import ( 20 "bytes" 21 "crypto/ecdsa" 22 "crypto/elliptic" 23 "crypto/rand" 24 "crypto/tls" 25 "crypto/x509" 26 "crypto/x509/pkix" 27 "encoding/json" 28 "encoding/pem" 29 "fmt" 30 "io/ioutil" 31 "math/big" 32 "net/http" 33 "net/http/httptest" 34 "reflect" 35 "strconv" 36 "strings" 37 "testing" 38 "time" 39 40 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 41 "k8s.io/apimachinery/pkg/runtime" 42 "k8s.io/apimachinery/pkg/util/clock" 43 "k8s.io/client-go/pkg/apis/clientauthentication" 44 "k8s.io/client-go/tools/clientcmd/api" 45 "k8s.io/client-go/transport" 46) 47 48var ( 49 certData = []byte(`-----BEGIN CERTIFICATE----- 50MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu 51MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz 52MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB 53BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v 54b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj 55lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2 56I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb 571Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F 58kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P 59AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ 60KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/ 61p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3 62jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq 636GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ 64HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ 65BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0 66-----END CERTIFICATE-----`) 67 keyData = []byte(`-----BEGIN RSA PRIVATE KEY----- 68MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i 69wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc 70kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG 710hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv 72RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi 73ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU 74FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK 75aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm 765mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M 77ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0 78JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr 797L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI 80cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey 81OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/ 82rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9 838PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg 84nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k 852bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII 86NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+ 87GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S 883OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG 8977uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/ 90bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/ 91F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX 92stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa 93-----END RSA PRIVATE KEY-----`) 94 validCert *tls.Certificate 95) 96 97func init() { 98 cert, err := tls.X509KeyPair(certData, keyData) 99 if err != nil { 100 panic(err) 101 } 102 cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 103 if err != nil { 104 panic(err) 105 } 106 validCert = &cert 107} 108 109func TestCacheKey(t *testing.T) { 110 c1 := &api.ExecConfig{ 111 Command: "foo-bar", 112 Args: []string{"1", "2"}, 113 Env: []api.ExecEnvVar{ 114 {Name: "3", Value: "4"}, 115 {Name: "5", Value: "6"}, 116 {Name: "7", Value: "8"}, 117 }, 118 APIVersion: "client.authentication.k8s.io/v1alpha1", 119 ProvideClusterInfo: true, 120 } 121 c1c := &clientauthentication.Cluster{ 122 Server: "foo", 123 TLSServerName: "bar", 124 CertificateAuthorityData: []byte("baz"), 125 Config: &runtime.Unknown{ 126 TypeMeta: runtime.TypeMeta{ 127 APIVersion: "", 128 Kind: "", 129 }, 130 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 131 ContentEncoding: "", 132 ContentType: "application/json", 133 }, 134 } 135 136 c2 := &api.ExecConfig{ 137 Command: "foo-bar", 138 Args: []string{"1", "2"}, 139 Env: []api.ExecEnvVar{ 140 {Name: "3", Value: "4"}, 141 {Name: "5", Value: "6"}, 142 {Name: "7", Value: "8"}, 143 }, 144 APIVersion: "client.authentication.k8s.io/v1alpha1", 145 ProvideClusterInfo: true, 146 } 147 c2c := &clientauthentication.Cluster{ 148 Server: "foo", 149 TLSServerName: "bar", 150 CertificateAuthorityData: []byte("baz"), 151 Config: &runtime.Unknown{ 152 TypeMeta: runtime.TypeMeta{ 153 APIVersion: "", 154 Kind: "", 155 }, 156 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 157 ContentEncoding: "", 158 ContentType: "application/json", 159 }, 160 } 161 162 c3 := &api.ExecConfig{ 163 Command: "foo-bar", 164 Args: []string{"1", "2"}, 165 Env: []api.ExecEnvVar{ 166 {Name: "3", Value: "4"}, 167 {Name: "5", Value: "6"}, 168 }, 169 APIVersion: "client.authentication.k8s.io/v1alpha1", 170 } 171 c3c := &clientauthentication.Cluster{ 172 Server: "foo", 173 TLSServerName: "bar", 174 CertificateAuthorityData: []byte("baz"), 175 Config: &runtime.Unknown{ 176 TypeMeta: runtime.TypeMeta{ 177 APIVersion: "", 178 Kind: "", 179 }, 180 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 181 ContentEncoding: "", 182 ContentType: "application/json", 183 }, 184 } 185 186 c4 := &api.ExecConfig{ 187 Command: "foo-bar", 188 Args: []string{"1", "2"}, 189 Env: []api.ExecEnvVar{ 190 {Name: "3", Value: "4"}, 191 {Name: "5", Value: "6"}, 192 }, 193 APIVersion: "client.authentication.k8s.io/v1alpha1", 194 } 195 c4c := &clientauthentication.Cluster{ 196 Server: "foo", 197 TLSServerName: "bar", 198 CertificateAuthorityData: []byte("baz"), 199 Config: &runtime.Unknown{ 200 TypeMeta: runtime.TypeMeta{ 201 APIVersion: "", 202 Kind: "", 203 }, 204 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`), 205 ContentEncoding: "", 206 ContentType: "application/json", 207 }, 208 } 209 210 // c5/c5c should be the same as c4/c4c, except c5 has ProvideClusterInfo set to true. 211 c5 := &api.ExecConfig{ 212 Command: "foo-bar", 213 Args: []string{"1", "2"}, 214 Env: []api.ExecEnvVar{ 215 {Name: "3", Value: "4"}, 216 {Name: "5", Value: "6"}, 217 }, 218 APIVersion: "client.authentication.k8s.io/v1alpha1", 219 ProvideClusterInfo: true, 220 } 221 c5c := &clientauthentication.Cluster{ 222 Server: "foo", 223 TLSServerName: "bar", 224 CertificateAuthorityData: []byte("baz"), 225 Config: &runtime.Unknown{ 226 TypeMeta: runtime.TypeMeta{ 227 APIVersion: "", 228 Kind: "", 229 }, 230 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`), 231 ContentEncoding: "", 232 ContentType: "application/json", 233 }, 234 } 235 236 // c6 should be the same as c4, except c6 is passed with a nil cluster 237 c6 := &api.ExecConfig{ 238 Command: "foo-bar", 239 Args: []string{"1", "2"}, 240 Env: []api.ExecEnvVar{ 241 {Name: "3", Value: "4"}, 242 {Name: "5", Value: "6"}, 243 }, 244 APIVersion: "client.authentication.k8s.io/v1alpha1", 245 } 246 247 key1 := cacheKey(c1, c1c) 248 key2 := cacheKey(c2, c2c) 249 key3 := cacheKey(c3, c3c) 250 key4 := cacheKey(c4, c4c) 251 key5 := cacheKey(c5, c5c) 252 key6 := cacheKey(c6, nil) 253 if key1 != key2 { 254 t.Error("key1 and key2 didn't match") 255 } 256 if key1 == key3 { 257 t.Error("key1 and key3 matched") 258 } 259 if key2 == key3 { 260 t.Error("key2 and key3 matched") 261 } 262 if key3 == key4 { 263 t.Error("key3 and key4 matched") 264 } 265 if key4 == key5 { 266 t.Error("key3 and key4 matched") 267 } 268 if key6 == key4 { 269 t.Error("key6 and key4 matched") 270 } 271} 272 273func compJSON(t *testing.T, got, want []byte) { 274 t.Helper() 275 gotJSON := &bytes.Buffer{} 276 wantJSON := &bytes.Buffer{} 277 278 if err := json.Indent(gotJSON, got, "", " "); err != nil { 279 t.Errorf("got invalid JSON: %v", err) 280 } 281 if err := json.Indent(wantJSON, want, "", " "); err != nil { 282 t.Errorf("want invalid JSON: %v", err) 283 } 284 g := strings.TrimSpace(gotJSON.String()) 285 w := strings.TrimSpace(wantJSON.String()) 286 if g != w { 287 t.Errorf("wanted %q, got %q", w, g) 288 } 289} 290 291func TestRefreshCreds(t *testing.T) { 292 tests := []struct { 293 name string 294 config api.ExecConfig 295 exitCode int 296 cluster *clientauthentication.Cluster 297 output string 298 interactive bool 299 response *clientauthentication.Response 300 wantInput string 301 wantCreds credentials 302 wantExpiry time.Time 303 wantErr bool 304 wantErrSubstr string 305 }{ 306 { 307 name: "basic-request", 308 config: api.ExecConfig{ 309 APIVersion: "client.authentication.k8s.io/v1alpha1", 310 }, 311 wantInput: `{ 312 "kind":"ExecCredential", 313 "apiVersion":"client.authentication.k8s.io/v1alpha1", 314 "spec": {} 315 }`, 316 output: `{ 317 "kind": "ExecCredential", 318 "apiVersion": "client.authentication.k8s.io/v1alpha1", 319 "status": { 320 "token": "foo-bar" 321 } 322 }`, 323 wantCreds: credentials{token: "foo-bar"}, 324 }, 325 { 326 name: "interactive", 327 config: api.ExecConfig{ 328 APIVersion: "client.authentication.k8s.io/v1alpha1", 329 }, 330 interactive: true, 331 wantInput: `{ 332 "kind":"ExecCredential", 333 "apiVersion":"client.authentication.k8s.io/v1alpha1", 334 "spec": { 335 "interactive": true 336 } 337 }`, 338 output: `{ 339 "kind": "ExecCredential", 340 "apiVersion": "client.authentication.k8s.io/v1alpha1", 341 "status": { 342 "token": "foo-bar" 343 } 344 }`, 345 wantCreds: credentials{token: "foo-bar"}, 346 }, 347 { 348 name: "response", 349 config: api.ExecConfig{ 350 APIVersion: "client.authentication.k8s.io/v1alpha1", 351 }, 352 response: &clientauthentication.Response{ 353 Header: map[string][]string{ 354 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 355 }, 356 Code: 401, 357 }, 358 wantInput: `{ 359 "kind":"ExecCredential", 360 "apiVersion":"client.authentication.k8s.io/v1alpha1", 361 "spec": { 362 "response": { 363 "header": { 364 "WWW-Authenticate": [ 365 "Basic realm=\"Access to the staging site\", charset=\"UTF-8\"" 366 ] 367 }, 368 "code": 401 369 } 370 } 371 }`, 372 output: `{ 373 "kind": "ExecCredential", 374 "apiVersion": "client.authentication.k8s.io/v1alpha1", 375 "status": { 376 "token": "foo-bar" 377 } 378 }`, 379 wantCreds: credentials{token: "foo-bar"}, 380 }, 381 { 382 name: "expiry", 383 config: api.ExecConfig{ 384 APIVersion: "client.authentication.k8s.io/v1alpha1", 385 }, 386 wantInput: `{ 387 "kind":"ExecCredential", 388 "apiVersion":"client.authentication.k8s.io/v1alpha1", 389 "spec": {} 390 }`, 391 output: `{ 392 "kind": "ExecCredential", 393 "apiVersion": "client.authentication.k8s.io/v1alpha1", 394 "status": { 395 "token": "foo-bar", 396 "expirationTimestamp": "2006-01-02T15:04:05Z" 397 } 398 }`, 399 wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), 400 wantCreds: credentials{token: "foo-bar"}, 401 }, 402 { 403 name: "no-group-version", 404 config: api.ExecConfig{ 405 APIVersion: "client.authentication.k8s.io/v1alpha1", 406 }, 407 wantInput: `{ 408 "kind":"ExecCredential", 409 "apiVersion":"client.authentication.k8s.io/v1alpha1", 410 "spec": {} 411 }`, 412 output: `{ 413 "kind": "ExecCredential", 414 "status": { 415 "token": "foo-bar" 416 } 417 }`, 418 wantErr: true, 419 }, 420 { 421 name: "no-status", 422 config: api.ExecConfig{ 423 APIVersion: "client.authentication.k8s.io/v1alpha1", 424 }, 425 wantInput: `{ 426 "kind":"ExecCredential", 427 "apiVersion":"client.authentication.k8s.io/v1alpha1", 428 "spec": {} 429 }`, 430 output: `{ 431 "kind": "ExecCredential", 432 "apiVersion":"client.authentication.k8s.io/v1alpha1" 433 }`, 434 wantErr: true, 435 }, 436 { 437 name: "no-creds", 438 config: api.ExecConfig{ 439 APIVersion: "client.authentication.k8s.io/v1alpha1", 440 }, 441 wantInput: `{ 442 "kind":"ExecCredential", 443 "apiVersion":"client.authentication.k8s.io/v1alpha1", 444 "spec": {} 445 }`, 446 output: `{ 447 "kind": "ExecCredential", 448 "apiVersion":"client.authentication.k8s.io/v1alpha1", 449 "status": {} 450 }`, 451 wantErr: true, 452 }, 453 { 454 name: "TLS credentials", 455 config: api.ExecConfig{ 456 APIVersion: "client.authentication.k8s.io/v1alpha1", 457 }, 458 wantInput: `{ 459 "kind":"ExecCredential", 460 "apiVersion":"client.authentication.k8s.io/v1alpha1", 461 "spec": {} 462 }`, 463 output: fmt.Sprintf(`{ 464 "kind": "ExecCredential", 465 "apiVersion": "client.authentication.k8s.io/v1alpha1", 466 "status": { 467 "clientKeyData": %q, 468 "clientCertificateData": %q 469 } 470 }`, keyData, certData), 471 wantCreds: credentials{cert: validCert}, 472 }, 473 { 474 name: "bad TLS credentials", 475 config: api.ExecConfig{ 476 APIVersion: "client.authentication.k8s.io/v1alpha1", 477 }, 478 wantInput: `{ 479 "kind":"ExecCredential", 480 "apiVersion":"client.authentication.k8s.io/v1alpha1", 481 "spec": {} 482 }`, 483 output: `{ 484 "kind": "ExecCredential", 485 "apiVersion": "client.authentication.k8s.io/v1alpha1", 486 "status": { 487 "clientKeyData": "foo", 488 "clientCertificateData": "bar" 489 } 490 }`, 491 wantErr: true, 492 }, 493 { 494 name: "cert but no key", 495 config: api.ExecConfig{ 496 APIVersion: "client.authentication.k8s.io/v1alpha1", 497 }, 498 wantInput: `{ 499 "kind":"ExecCredential", 500 "apiVersion":"client.authentication.k8s.io/v1alpha1", 501 "spec": {} 502 }`, 503 output: fmt.Sprintf(`{ 504 "kind": "ExecCredential", 505 "apiVersion": "client.authentication.k8s.io/v1alpha1", 506 "status": { 507 "clientCertificateData": %q 508 } 509 }`, certData), 510 wantErr: true, 511 }, 512 { 513 name: "beta-basic-request", 514 config: api.ExecConfig{ 515 APIVersion: "client.authentication.k8s.io/v1beta1", 516 }, 517 wantInput: `{ 518 "kind": "ExecCredential", 519 "apiVersion": "client.authentication.k8s.io/v1beta1", 520 "spec": {} 521 }`, 522 output: `{ 523 "kind": "ExecCredential", 524 "apiVersion": "client.authentication.k8s.io/v1beta1", 525 "status": { 526 "token": "foo-bar" 527 } 528 }`, 529 wantCreds: credentials{token: "foo-bar"}, 530 }, 531 { 532 name: "beta-expiry", 533 config: api.ExecConfig{ 534 APIVersion: "client.authentication.k8s.io/v1beta1", 535 }, 536 wantInput: `{ 537 "kind": "ExecCredential", 538 "apiVersion": "client.authentication.k8s.io/v1beta1", 539 "spec": {} 540 }`, 541 output: `{ 542 "kind": "ExecCredential", 543 "apiVersion": "client.authentication.k8s.io/v1beta1", 544 "status": { 545 "token": "foo-bar", 546 "expirationTimestamp": "2006-01-02T15:04:05Z" 547 } 548 }`, 549 wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), 550 wantCreds: credentials{token: "foo-bar"}, 551 }, 552 { 553 name: "beta-no-group-version", 554 config: api.ExecConfig{ 555 APIVersion: "client.authentication.k8s.io/v1beta1", 556 }, 557 output: `{ 558 "kind": "ExecCredential", 559 "status": { 560 "token": "foo-bar" 561 } 562 }`, 563 wantErr: true, 564 }, 565 { 566 name: "beta-no-status", 567 config: api.ExecConfig{ 568 APIVersion: "client.authentication.k8s.io/v1beta1", 569 }, 570 output: `{ 571 "kind": "ExecCredential", 572 "apiVersion":"client.authentication.k8s.io/v1beta1" 573 }`, 574 wantErr: true, 575 }, 576 { 577 name: "beta-no-token", 578 config: api.ExecConfig{ 579 APIVersion: "client.authentication.k8s.io/v1beta1", 580 }, 581 output: `{ 582 "kind": "ExecCredential", 583 "apiVersion":"client.authentication.k8s.io/v1beta1", 584 "status": {} 585 }`, 586 wantErr: true, 587 }, 588 { 589 name: "unknown-binary", 590 config: api.ExecConfig{ 591 APIVersion: "client.authentication.k8s.io/v1beta1", 592 Command: "does not exist", 593 InstallHint: "some install hint", 594 }, 595 wantErr: true, 596 wantErrSubstr: "some install hint", 597 }, 598 { 599 name: "binary-fails", 600 config: api.ExecConfig{ 601 APIVersion: "client.authentication.k8s.io/v1beta1", 602 }, 603 exitCode: 73, 604 wantErr: true, 605 wantErrSubstr: "73", 606 }, 607 { 608 name: "alpha-with-cluster-is-ignored", 609 config: api.ExecConfig{ 610 APIVersion: "client.authentication.k8s.io/v1alpha1", 611 }, 612 cluster: &clientauthentication.Cluster{ 613 Server: "foo", 614 TLSServerName: "bar", 615 CertificateAuthorityData: []byte("baz"), 616 Config: &runtime.Unknown{ 617 TypeMeta: runtime.TypeMeta{ 618 APIVersion: "", 619 Kind: "", 620 }, 621 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`), 622 ContentEncoding: "", 623 ContentType: "application/json", 624 }, 625 }, 626 response: &clientauthentication.Response{ 627 Header: map[string][]string{ 628 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 629 }, 630 Code: 401, 631 }, 632 wantInput: `{ 633 "kind":"ExecCredential", 634 "apiVersion":"client.authentication.k8s.io/v1alpha1", 635 "spec": { 636 "response": { 637 "header": { 638 "WWW-Authenticate": [ 639 "Basic realm=\"Access to the staging site\", charset=\"UTF-8\"" 640 ] 641 }, 642 "code": 401 643 } 644 } 645 }`, 646 output: `{ 647 "kind": "ExecCredential", 648 "apiVersion": "client.authentication.k8s.io/v1alpha1", 649 "status": { 650 "token": "foo-bar" 651 } 652 }`, 653 wantCreds: credentials{token: "foo-bar"}, 654 }, 655 { 656 name: "beta-with-cluster-and-provide-cluster-info-is-serialized", 657 config: api.ExecConfig{ 658 APIVersion: "client.authentication.k8s.io/v1beta1", 659 ProvideClusterInfo: true, 660 }, 661 cluster: &clientauthentication.Cluster{ 662 Server: "foo", 663 TLSServerName: "bar", 664 CertificateAuthorityData: []byte("baz"), 665 Config: &runtime.Unknown{ 666 TypeMeta: runtime.TypeMeta{ 667 APIVersion: "", 668 Kind: "", 669 }, 670 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 671 ContentEncoding: "", 672 ContentType: "application/json", 673 }, 674 }, 675 response: &clientauthentication.Response{ 676 Header: map[string][]string{ 677 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 678 }, 679 Code: 401, 680 }, 681 wantInput: `{ 682 "kind":"ExecCredential", 683 "apiVersion":"client.authentication.k8s.io/v1beta1", 684 "spec": { 685 "cluster": { 686 "server": "foo", 687 "tls-server-name": "bar", 688 "certificate-authority-data": "YmF6", 689 "config": { 690 "apiVersion": "group/v1", 691 "kind": "PluginConfig", 692 "spec": { 693 "audience": "snorlax" 694 } 695 } 696 } 697 } 698 }`, 699 output: `{ 700 "kind": "ExecCredential", 701 "apiVersion": "client.authentication.k8s.io/v1beta1", 702 "status": { 703 "token": "foo-bar" 704 } 705 }`, 706 wantCreds: credentials{token: "foo-bar"}, 707 }, 708 { 709 name: "beta-with-cluster-and-without-provide-cluster-info-is-not-serialized", 710 config: api.ExecConfig{ 711 APIVersion: "client.authentication.k8s.io/v1beta1", 712 }, 713 cluster: &clientauthentication.Cluster{ 714 Server: "foo", 715 TLSServerName: "bar", 716 CertificateAuthorityData: []byte("baz"), 717 Config: &runtime.Unknown{ 718 TypeMeta: runtime.TypeMeta{ 719 APIVersion: "", 720 Kind: "", 721 }, 722 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 723 ContentEncoding: "", 724 ContentType: "application/json", 725 }, 726 }, 727 response: &clientauthentication.Response{ 728 Header: map[string][]string{ 729 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 730 }, 731 Code: 401, 732 }, 733 wantInput: `{ 734 "kind":"ExecCredential", 735 "apiVersion":"client.authentication.k8s.io/v1beta1", 736 "spec": {} 737 }`, 738 output: `{ 739 "kind": "ExecCredential", 740 "apiVersion": "client.authentication.k8s.io/v1beta1", 741 "status": { 742 "token": "foo-bar" 743 } 744 }`, 745 wantCreds: credentials{token: "foo-bar"}, 746 }, 747 } 748 749 for _, test := range tests { 750 t.Run(test.name, func(t *testing.T) { 751 c := test.config 752 753 if c.Command == "" { 754 c.Command = "./testdata/test-plugin.sh" 755 c.Env = append(c.Env, api.ExecEnvVar{ 756 Name: "TEST_OUTPUT", 757 Value: test.output, 758 }) 759 c.Env = append(c.Env, api.ExecEnvVar{ 760 Name: "TEST_EXIT_CODE", 761 Value: strconv.Itoa(test.exitCode), 762 }) 763 } 764 765 a, err := newAuthenticator(newCache(), &c, test.cluster) 766 if err != nil { 767 t.Fatal(err) 768 } 769 770 stderr := &bytes.Buffer{} 771 a.stderr = stderr 772 a.interactive = test.interactive 773 a.environ = func() []string { return nil } 774 775 if err := a.refreshCredsLocked(test.response); err != nil { 776 if !test.wantErr { 777 t.Errorf("get token %v", err) 778 } else if !strings.Contains(err.Error(), test.wantErrSubstr) { 779 t.Errorf("expected error with substring '%v' got '%v'", test.wantErrSubstr, err.Error()) 780 } 781 return 782 } 783 if test.wantErr { 784 t.Fatal("expected error getting token") 785 } 786 787 if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) { 788 t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds) 789 } 790 791 if !a.exp.Equal(test.wantExpiry) { 792 t.Errorf("expected expiry %v got %v", test.wantExpiry, a.exp) 793 } 794 795 if test.wantInput == "" { 796 if got := strings.TrimSpace(stderr.String()); got != "" { 797 t.Errorf("expected no input parameters, got %q", got) 798 } 799 return 800 } 801 802 compJSON(t, stderr.Bytes(), []byte(test.wantInput)) 803 }) 804 } 805} 806 807func TestRoundTripper(t *testing.T) { 808 wantToken := "" 809 810 n := time.Now() 811 now := func() time.Time { return n } 812 813 env := []string{""} 814 environ := func() []string { 815 s := make([]string, len(env)) 816 copy(s, env) 817 return s 818 } 819 820 setOutput := func(s string) { 821 env[0] = "TEST_OUTPUT=" + s 822 } 823 824 handler := func(w http.ResponseWriter, r *http.Request) { 825 gotToken := "" 826 parts := strings.Split(r.Header.Get("Authorization"), " ") 827 if len(parts) > 1 && strings.EqualFold(parts[0], "bearer") { 828 gotToken = parts[1] 829 } 830 831 if wantToken != gotToken { 832 http.Error(w, "Unauthorized", http.StatusUnauthorized) 833 return 834 } 835 fmt.Fprintln(w, "ok") 836 } 837 server := httptest.NewServer(http.HandlerFunc(handler)) 838 839 c := api.ExecConfig{ 840 Command: "./testdata/test-plugin.sh", 841 APIVersion: "client.authentication.k8s.io/v1alpha1", 842 } 843 a, err := newAuthenticator(newCache(), &c, nil) 844 if err != nil { 845 t.Fatal(err) 846 } 847 a.environ = environ 848 a.now = now 849 a.stderr = ioutil.Discard 850 851 tc := &transport.Config{} 852 if err := a.UpdateTransportConfig(tc); err != nil { 853 t.Fatal(err) 854 } 855 client := http.Client{ 856 Transport: tc.WrapTransport(http.DefaultTransport), 857 } 858 859 get := func(t *testing.T, statusCode int) { 860 t.Helper() 861 resp, err := client.Get(server.URL) 862 if err != nil { 863 t.Fatal(err) 864 } 865 defer resp.Body.Close() 866 if resp.StatusCode != statusCode { 867 t.Errorf("wanted status %d got %d", statusCode, resp.StatusCode) 868 } 869 } 870 871 setOutput(`{ 872 "kind": "ExecCredential", 873 "apiVersion": "client.authentication.k8s.io/v1alpha1", 874 "status": { 875 "token": "token1" 876 } 877 }`) 878 wantToken = "token1" 879 get(t, http.StatusOK) 880 881 setOutput(`{ 882 "kind": "ExecCredential", 883 "apiVersion": "client.authentication.k8s.io/v1alpha1", 884 "status": { 885 "token": "token2" 886 } 887 }`) 888 // Previous token should be cached 889 get(t, http.StatusOK) 890 891 wantToken = "token2" 892 // Token is still cached, hits unauthorized but causes token to rotate. 893 get(t, http.StatusUnauthorized) 894 // Follow up request uses the rotated token. 895 get(t, http.StatusOK) 896 897 setOutput(`{ 898 "kind": "ExecCredential", 899 "apiVersion": "client.authentication.k8s.io/v1alpha1", 900 "status": { 901 "token": "token3", 902 "expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `" 903 } 904 }`) 905 wantToken = "token3" 906 // Token is still cached, hit's unauthorized but causes rotation to token with an expiry. 907 get(t, http.StatusUnauthorized) 908 get(t, http.StatusOK) 909 910 // Move time forward 2 hours, "token3" is now expired. 911 n = n.Add(time.Hour * 2) 912 setOutput(`{ 913 "kind": "ExecCredential", 914 "apiVersion": "client.authentication.k8s.io/v1alpha1", 915 "status": { 916 "token": "token4", 917 "expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `" 918 } 919 }`) 920 wantToken = "token4" 921 // Old token is expired, should refresh automatically without hitting a 401. 922 get(t, http.StatusOK) 923} 924 925func TestTokenPresentCancelsExecAction(t *testing.T) { 926 a, err := newAuthenticator(newCache(), &api.ExecConfig{ 927 Command: "./testdata/test-plugin.sh", 928 APIVersion: "client.authentication.k8s.io/v1alpha1", 929 }, nil) 930 if err != nil { 931 t.Fatal(err) 932 } 933 934 // UpdateTransportConfig returns error on existing TLS certificate callback, unless a bearer token is present in the 935 // transport config, in which case it takes precedence 936 cert := func() (*tls.Certificate, error) { 937 return nil, nil 938 } 939 tc := &transport.Config{BearerToken: "token1", TLS: transport.TLSConfig{Insecure: true, GetCert: cert}} 940 941 if err := a.UpdateTransportConfig(tc); err != nil { 942 t.Error("Expected presence of bearer token in config to cancel exec action") 943 } 944} 945 946func TestTLSCredentials(t *testing.T) { 947 now := time.Now() 948 949 certPool := x509.NewCertPool() 950 cert, key := genClientCert(t) 951 if !certPool.AppendCertsFromPEM(cert) { 952 t.Fatal("failed to add client cert to CertPool") 953 } 954 955 server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 956 fmt.Fprintln(w, "ok") 957 })) 958 server.TLS = &tls.Config{ 959 ClientAuth: tls.RequireAndVerifyClientCert, 960 ClientCAs: certPool, 961 } 962 server.StartTLS() 963 defer server.Close() 964 965 a, err := newAuthenticator(newCache(), &api.ExecConfig{ 966 Command: "./testdata/test-plugin.sh", 967 APIVersion: "client.authentication.k8s.io/v1alpha1", 968 }, nil) 969 if err != nil { 970 t.Fatal(err) 971 } 972 var output *clientauthentication.ExecCredential 973 a.environ = func() []string { 974 data, err := runtime.Encode(codecs.LegacyCodec(a.group), output) 975 if err != nil { 976 t.Fatal(err) 977 } 978 return []string{"TEST_OUTPUT=" + string(data)} 979 } 980 a.now = func() time.Time { return now } 981 a.stderr = ioutil.Discard 982 983 // We're not interested in server's cert, this test is about client cert. 984 tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true}} 985 if err := a.UpdateTransportConfig(tc); err != nil { 986 t.Fatal(err) 987 } 988 989 get := func(t *testing.T, desc string, wantErr bool) { 990 t.Run(desc, func(t *testing.T) { 991 tlsCfg, err := transport.TLSConfigFor(tc) 992 if err != nil { 993 t.Fatal("TLSConfigFor:", err) 994 } 995 client := http.Client{ 996 Transport: &http.Transport{TLSClientConfig: tlsCfg}, 997 } 998 resp, err := client.Get(server.URL) 999 switch { 1000 case err != nil && !wantErr: 1001 t.Errorf("got client.Get error: %q, want nil", err) 1002 case err == nil && wantErr: 1003 t.Error("got nil client.Get error, want non-nil") 1004 } 1005 if err == nil { 1006 resp.Body.Close() 1007 } 1008 }) 1009 } 1010 1011 output = &clientauthentication.ExecCredential{ 1012 Status: &clientauthentication.ExecCredentialStatus{ 1013 ClientCertificateData: string(cert), 1014 ClientKeyData: string(key), 1015 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 1016 }, 1017 } 1018 get(t, "valid TLS cert", false) 1019 1020 // Advance time to force re-exec. 1021 nCert, nKey := genClientCert(t) 1022 now = now.Add(time.Hour * 2) 1023 output = &clientauthentication.ExecCredential{ 1024 Status: &clientauthentication.ExecCredentialStatus{ 1025 ClientCertificateData: string(nCert), 1026 ClientKeyData: string(nKey), 1027 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 1028 }, 1029 } 1030 get(t, "untrusted TLS cert", true) 1031 1032 now = now.Add(time.Hour * 2) 1033 output = &clientauthentication.ExecCredential{ 1034 Status: &clientauthentication.ExecCredentialStatus{ 1035 ClientCertificateData: string(cert), 1036 ClientKeyData: string(key), 1037 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 1038 }, 1039 } 1040 get(t, "valid TLS cert again", false) 1041} 1042 1043func TestConcurrentUpdateTransportConfig(t *testing.T) { 1044 n := time.Now() 1045 now := func() time.Time { return n } 1046 1047 env := []string{""} 1048 environ := func() []string { 1049 s := make([]string, len(env)) 1050 copy(s, env) 1051 return s 1052 } 1053 1054 c := api.ExecConfig{ 1055 Command: "./testdata/test-plugin.sh", 1056 APIVersion: "client.authentication.k8s.io/v1alpha1", 1057 } 1058 a, err := newAuthenticator(newCache(), &c, nil) 1059 if err != nil { 1060 t.Fatal(err) 1061 } 1062 a.environ = environ 1063 a.now = now 1064 a.stderr = ioutil.Discard 1065 1066 stopCh := make(chan struct{}) 1067 defer close(stopCh) 1068 1069 numConcurrent := 2 1070 1071 for i := 0; i < numConcurrent; i++ { 1072 go func() { 1073 for { 1074 tc := &transport.Config{} 1075 a.UpdateTransportConfig(tc) 1076 1077 select { 1078 case <-stopCh: 1079 return 1080 default: 1081 continue 1082 } 1083 } 1084 }() 1085 } 1086 time.Sleep(2 * time.Second) 1087} 1088 1089func TestInstallHintRateLimit(t *testing.T) { 1090 tests := []struct { 1091 name string 1092 1093 threshold int 1094 interval time.Duration 1095 1096 calls int 1097 perCallAdvance time.Duration 1098 1099 wantInstallHint int 1100 }{ 1101 { 1102 name: "print-up-to-threshold", 1103 threshold: 2, 1104 interval: time.Second, 1105 calls: 10, 1106 wantInstallHint: 2, 1107 }, 1108 { 1109 name: "after-interval-threshold-resets", 1110 threshold: 2, 1111 interval: time.Second * 5, 1112 calls: 10, 1113 perCallAdvance: time.Second, 1114 wantInstallHint: 4, 1115 }, 1116 } 1117 1118 for _, test := range tests { 1119 t.Run(test.name, func(t *testing.T) { 1120 c := api.ExecConfig{ 1121 Command: "does not exist", 1122 APIVersion: "client.authentication.k8s.io/v1alpha1", 1123 InstallHint: "some install hint", 1124 } 1125 a, err := newAuthenticator(newCache(), &c, nil) 1126 if err != nil { 1127 t.Fatal(err) 1128 } 1129 1130 a.sometimes.threshold = test.threshold 1131 a.sometimes.interval = test.interval 1132 1133 clock := clock.NewFakeClock(time.Now()) 1134 a.sometimes.clock = clock 1135 1136 count := 0 1137 for i := 0; i < test.calls; i++ { 1138 err := a.refreshCredsLocked(&clientauthentication.Response{}) 1139 if strings.Contains(err.Error(), c.InstallHint) { 1140 count++ 1141 } 1142 1143 clock.SetTime(clock.Now().Add(test.perCallAdvance)) 1144 } 1145 1146 if test.wantInstallHint != count { 1147 t.Errorf( 1148 "%s: expected install hint %d times got %d", 1149 test.name, 1150 test.wantInstallHint, 1151 count, 1152 ) 1153 } 1154 }) 1155 } 1156} 1157 1158// genClientCert generates an x509 certificate for testing. Certificate and key 1159// are returned in PEM encoding. The generated cert expires in 24 hours. 1160func genClientCert(t *testing.T) ([]byte, []byte) { 1161 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 1162 if err != nil { 1163 t.Fatal(err) 1164 } 1165 keyRaw, err := x509.MarshalECPrivateKey(key) 1166 if err != nil { 1167 t.Fatal(err) 1168 } 1169 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 1170 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 1171 if err != nil { 1172 t.Fatal(err) 1173 } 1174 cert := &x509.Certificate{ 1175 SerialNumber: serialNumber, 1176 Subject: pkix.Name{Organization: []string{"Acme Co"}}, 1177 NotBefore: time.Now(), 1178 NotAfter: time.Now().Add(24 * time.Hour), 1179 1180 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 1181 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 1182 BasicConstraintsValid: true, 1183 } 1184 certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) 1185 if err != nil { 1186 t.Fatal(err) 1187 } 1188 return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}), 1189 pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw}) 1190} 1191