k8s.io/client-go@v0.22.2/plugin/pkg/client/auth/exec/exec_test.go (about) 1 /* 2 Copyright 2018 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 exec 18 19 import ( 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 48 var ( 49 certData = []byte(`-----BEGIN CERTIFICATE----- 50 MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu 51 MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz 52 MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB 53 BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v 54 b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj 55 lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2 56 I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb 57 1Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F 58 kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P 59 AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ 60 KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/ 61 p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3 62 jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq 63 6GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ 64 HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ 65 BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0 66 -----END CERTIFICATE-----`) 67 keyData = []byte(`-----BEGIN RSA PRIVATE KEY----- 68 MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i 69 wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc 70 kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG 71 0hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv 72 RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi 73 ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU 74 FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK 75 aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm 76 5mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M 77 ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0 78 JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr 79 7L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI 80 cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey 81 OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/ 82 rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9 83 8PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg 84 nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k 85 2bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII 86 NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+ 87 GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S 88 3OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG 89 77uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/ 90 bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/ 91 F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX 92 stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa 93 -----END RSA PRIVATE KEY-----`) 94 validCert *tls.Certificate 95 ) 96 97 func 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 109 func 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 // c7 should be the same as c6, except c7 has stdin marked as unavailable 248 c7 := &api.ExecConfig{ 249 Command: "foo-bar", 250 Args: []string{"1", "2"}, 251 Env: []api.ExecEnvVar{ 252 {Name: "3", Value: "4"}, 253 {Name: "5", Value: "6"}, 254 }, 255 APIVersion: "client.authentication.k8s.io/v1alpha1", 256 StdinUnavailable: true, 257 } 258 259 key1 := cacheKey(c1, c1c) 260 key2 := cacheKey(c2, c2c) 261 key3 := cacheKey(c3, c3c) 262 key4 := cacheKey(c4, c4c) 263 key5 := cacheKey(c5, c5c) 264 key6 := cacheKey(c6, nil) 265 key7 := cacheKey(c7, nil) 266 if key1 != key2 { 267 t.Error("key1 and key2 didn't match") 268 } 269 if key1 == key3 { 270 t.Error("key1 and key3 matched") 271 } 272 if key2 == key3 { 273 t.Error("key2 and key3 matched") 274 } 275 if key3 == key4 { 276 t.Error("key3 and key4 matched") 277 } 278 if key4 == key5 { 279 t.Error("key3 and key4 matched") 280 } 281 if key6 == key4 { 282 t.Error("key6 and key4 matched") 283 } 284 if key6 == key7 { 285 t.Error("key6 and key7 matched") 286 } 287 } 288 289 func compJSON(t *testing.T, got, want []byte) { 290 t.Helper() 291 gotJSON := &bytes.Buffer{} 292 wantJSON := &bytes.Buffer{} 293 294 if err := json.Indent(gotJSON, got, "", " "); err != nil { 295 t.Errorf("got invalid JSON: %v", err) 296 } 297 if err := json.Indent(wantJSON, want, "", " "); err != nil { 298 t.Errorf("want invalid JSON: %v", err) 299 } 300 g := strings.TrimSpace(gotJSON.String()) 301 w := strings.TrimSpace(wantJSON.String()) 302 if g != w { 303 t.Errorf("wanted %q, got %q", w, g) 304 } 305 } 306 307 func TestRefreshCreds(t *testing.T) { 308 tests := []struct { 309 name string 310 config api.ExecConfig 311 stdinUnavailable bool 312 exitCode int 313 cluster *clientauthentication.Cluster 314 output string 315 isTerminal bool 316 response *clientauthentication.Response 317 wantInput string 318 wantCreds credentials 319 wantExpiry time.Time 320 wantErr bool 321 wantErrSubstr string 322 }{ 323 { 324 name: "basic-request", 325 config: api.ExecConfig{ 326 APIVersion: "client.authentication.k8s.io/v1alpha1", 327 InteractiveMode: api.IfAvailableExecInteractiveMode, 328 }, 329 wantInput: `{ 330 "kind":"ExecCredential", 331 "apiVersion":"client.authentication.k8s.io/v1alpha1", 332 "spec": {} 333 }`, 334 output: `{ 335 "kind": "ExecCredential", 336 "apiVersion": "client.authentication.k8s.io/v1alpha1", 337 "status": { 338 "token": "foo-bar" 339 } 340 }`, 341 wantCreds: credentials{token: "foo-bar"}, 342 }, 343 { 344 name: "interactive", 345 config: api.ExecConfig{ 346 APIVersion: "client.authentication.k8s.io/v1alpha1", 347 InteractiveMode: api.IfAvailableExecInteractiveMode, 348 }, 349 isTerminal: true, 350 wantInput: `{ 351 "kind":"ExecCredential", 352 "apiVersion":"client.authentication.k8s.io/v1alpha1", 353 "spec": { 354 "interactive": true 355 } 356 }`, 357 output: `{ 358 "kind": "ExecCredential", 359 "apiVersion": "client.authentication.k8s.io/v1alpha1", 360 "status": { 361 "token": "foo-bar" 362 } 363 }`, 364 wantCreds: credentials{token: "foo-bar"}, 365 }, 366 { 367 name: "response", 368 config: api.ExecConfig{ 369 APIVersion: "client.authentication.k8s.io/v1alpha1", 370 InteractiveMode: api.IfAvailableExecInteractiveMode, 371 }, 372 response: &clientauthentication.Response{ 373 Header: map[string][]string{ 374 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 375 }, 376 Code: 401, 377 }, 378 wantInput: `{ 379 "kind":"ExecCredential", 380 "apiVersion":"client.authentication.k8s.io/v1alpha1", 381 "spec": { 382 "response": { 383 "header": { 384 "WWW-Authenticate": [ 385 "Basic realm=\"Access to the staging site\", charset=\"UTF-8\"" 386 ] 387 }, 388 "code": 401 389 } 390 } 391 }`, 392 output: `{ 393 "kind": "ExecCredential", 394 "apiVersion": "client.authentication.k8s.io/v1alpha1", 395 "status": { 396 "token": "foo-bar" 397 } 398 }`, 399 wantCreds: credentials{token: "foo-bar"}, 400 }, 401 { 402 name: "expiry", 403 config: api.ExecConfig{ 404 APIVersion: "client.authentication.k8s.io/v1alpha1", 405 InteractiveMode: api.IfAvailableExecInteractiveMode, 406 }, 407 wantInput: `{ 408 "kind":"ExecCredential", 409 "apiVersion":"client.authentication.k8s.io/v1alpha1", 410 "spec": {} 411 }`, 412 output: `{ 413 "kind": "ExecCredential", 414 "apiVersion": "client.authentication.k8s.io/v1alpha1", 415 "status": { 416 "token": "foo-bar", 417 "expirationTimestamp": "2006-01-02T15:04:05Z" 418 } 419 }`, 420 wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), 421 wantCreds: credentials{token: "foo-bar"}, 422 }, 423 { 424 name: "no-group-version", 425 config: api.ExecConfig{ 426 APIVersion: "client.authentication.k8s.io/v1alpha1", 427 InteractiveMode: api.IfAvailableExecInteractiveMode, 428 }, 429 wantInput: `{ 430 "kind":"ExecCredential", 431 "apiVersion":"client.authentication.k8s.io/v1alpha1", 432 "spec": {} 433 }`, 434 output: `{ 435 "kind": "ExecCredential", 436 "status": { 437 "token": "foo-bar" 438 } 439 }`, 440 wantErr: true, 441 }, 442 { 443 name: "no-status", 444 config: api.ExecConfig{ 445 APIVersion: "client.authentication.k8s.io/v1alpha1", 446 InteractiveMode: api.IfAvailableExecInteractiveMode, 447 }, 448 wantInput: `{ 449 "kind":"ExecCredential", 450 "apiVersion":"client.authentication.k8s.io/v1alpha1", 451 "spec": {} 452 }`, 453 output: `{ 454 "kind": "ExecCredential", 455 "apiVersion":"client.authentication.k8s.io/v1alpha1" 456 }`, 457 wantErr: true, 458 }, 459 { 460 name: "no-creds", 461 config: api.ExecConfig{ 462 APIVersion: "client.authentication.k8s.io/v1alpha1", 463 InteractiveMode: api.IfAvailableExecInteractiveMode, 464 }, 465 wantInput: `{ 466 "kind":"ExecCredential", 467 "apiVersion":"client.authentication.k8s.io/v1alpha1", 468 "spec": {} 469 }`, 470 output: `{ 471 "kind": "ExecCredential", 472 "apiVersion":"client.authentication.k8s.io/v1alpha1", 473 "status": {} 474 }`, 475 wantErr: true, 476 }, 477 { 478 name: "TLS credentials", 479 config: api.ExecConfig{ 480 APIVersion: "client.authentication.k8s.io/v1alpha1", 481 InteractiveMode: api.IfAvailableExecInteractiveMode, 482 }, 483 wantInput: `{ 484 "kind":"ExecCredential", 485 "apiVersion":"client.authentication.k8s.io/v1alpha1", 486 "spec": {} 487 }`, 488 output: fmt.Sprintf(`{ 489 "kind": "ExecCredential", 490 "apiVersion": "client.authentication.k8s.io/v1alpha1", 491 "status": { 492 "clientKeyData": %q, 493 "clientCertificateData": %q 494 } 495 }`, keyData, certData), 496 wantCreds: credentials{cert: validCert}, 497 }, 498 { 499 name: "bad TLS credentials", 500 config: api.ExecConfig{ 501 APIVersion: "client.authentication.k8s.io/v1alpha1", 502 InteractiveMode: api.IfAvailableExecInteractiveMode, 503 }, 504 wantInput: `{ 505 "kind":"ExecCredential", 506 "apiVersion":"client.authentication.k8s.io/v1alpha1", 507 "spec": {} 508 }`, 509 output: `{ 510 "kind": "ExecCredential", 511 "apiVersion": "client.authentication.k8s.io/v1alpha1", 512 "status": { 513 "clientKeyData": "foo", 514 "clientCertificateData": "bar" 515 } 516 }`, 517 wantErr: true, 518 }, 519 { 520 name: "cert but no key", 521 config: api.ExecConfig{ 522 APIVersion: "client.authentication.k8s.io/v1alpha1", 523 InteractiveMode: api.IfAvailableExecInteractiveMode, 524 }, 525 wantInput: `{ 526 "kind":"ExecCredential", 527 "apiVersion":"client.authentication.k8s.io/v1alpha1", 528 "spec": {} 529 }`, 530 output: fmt.Sprintf(`{ 531 "kind": "ExecCredential", 532 "apiVersion": "client.authentication.k8s.io/v1alpha1", 533 "status": { 534 "clientCertificateData": %q 535 } 536 }`, certData), 537 wantErr: true, 538 }, 539 { 540 name: "beta-basic-request", 541 config: api.ExecConfig{ 542 APIVersion: "client.authentication.k8s.io/v1beta1", 543 InteractiveMode: api.IfAvailableExecInteractiveMode, 544 }, 545 wantInput: `{ 546 "kind": "ExecCredential", 547 "apiVersion": "client.authentication.k8s.io/v1beta1", 548 "spec": { 549 "interactive": false 550 } 551 }`, 552 output: `{ 553 "kind": "ExecCredential", 554 "apiVersion": "client.authentication.k8s.io/v1beta1", 555 "status": { 556 "token": "foo-bar" 557 } 558 }`, 559 wantCreds: credentials{token: "foo-bar"}, 560 }, 561 { 562 name: "beta-basic-request-with-never-interactive-mode", 563 config: api.ExecConfig{ 564 APIVersion: "client.authentication.k8s.io/v1beta1", 565 InteractiveMode: api.NeverExecInteractiveMode, 566 }, 567 wantInput: `{ 568 "kind": "ExecCredential", 569 "apiVersion": "client.authentication.k8s.io/v1beta1", 570 "spec": { 571 "interactive": false 572 } 573 }`, 574 output: `{ 575 "kind": "ExecCredential", 576 "apiVersion": "client.authentication.k8s.io/v1beta1", 577 "status": { 578 "token": "foo-bar" 579 } 580 }`, 581 wantCreds: credentials{token: "foo-bar"}, 582 }, 583 { 584 name: "beta-basic-request-with-never-interactive-mode-and-stdin-unavailable", 585 config: api.ExecConfig{ 586 APIVersion: "client.authentication.k8s.io/v1beta1", 587 InteractiveMode: api.NeverExecInteractiveMode, 588 StdinUnavailable: true, 589 }, 590 wantInput: `{ 591 "kind": "ExecCredential", 592 "apiVersion": "client.authentication.k8s.io/v1beta1", 593 "spec": { 594 "interactive": false 595 } 596 }`, 597 output: `{ 598 "kind": "ExecCredential", 599 "apiVersion": "client.authentication.k8s.io/v1beta1", 600 "status": { 601 "token": "foo-bar" 602 } 603 }`, 604 wantCreds: credentials{token: "foo-bar"}, 605 }, 606 { 607 name: "beta-basic-request-with-if-available-interactive-mode", 608 config: api.ExecConfig{ 609 APIVersion: "client.authentication.k8s.io/v1beta1", 610 InteractiveMode: api.IfAvailableExecInteractiveMode, 611 }, 612 wantInput: `{ 613 "kind": "ExecCredential", 614 "apiVersion": "client.authentication.k8s.io/v1beta1", 615 "spec": { 616 "interactive": false 617 } 618 }`, 619 output: `{ 620 "kind": "ExecCredential", 621 "apiVersion": "client.authentication.k8s.io/v1beta1", 622 "status": { 623 "token": "foo-bar" 624 } 625 }`, 626 wantCreds: credentials{token: "foo-bar"}, 627 }, 628 { 629 name: "beta-basic-request-with-if-available-interactive-mode-and-stdin-unavailable", 630 config: api.ExecConfig{ 631 APIVersion: "client.authentication.k8s.io/v1beta1", 632 InteractiveMode: api.IfAvailableExecInteractiveMode, 633 StdinUnavailable: true, 634 }, 635 wantInput: `{ 636 "kind": "ExecCredential", 637 "apiVersion": "client.authentication.k8s.io/v1beta1", 638 "spec": { 639 "interactive": false 640 } 641 }`, 642 output: `{ 643 "kind": "ExecCredential", 644 "apiVersion": "client.authentication.k8s.io/v1beta1", 645 "status": { 646 "token": "foo-bar" 647 } 648 }`, 649 wantCreds: credentials{token: "foo-bar"}, 650 }, 651 { 652 name: "beta-basic-request-with-if-available-interactive-mode-and-terminal", 653 config: api.ExecConfig{ 654 APIVersion: "client.authentication.k8s.io/v1beta1", 655 InteractiveMode: api.IfAvailableExecInteractiveMode, 656 }, 657 isTerminal: true, 658 wantInput: `{ 659 "kind": "ExecCredential", 660 "apiVersion": "client.authentication.k8s.io/v1beta1", 661 "spec": { 662 "interactive": true 663 } 664 }`, 665 output: `{ 666 "kind": "ExecCredential", 667 "apiVersion": "client.authentication.k8s.io/v1beta1", 668 "status": { 669 "token": "foo-bar" 670 } 671 }`, 672 wantCreds: credentials{token: "foo-bar"}, 673 }, 674 { 675 name: "beta-basic-request-with-if-available-interactive-mode-and-terminal-and-stdin-unavailable", 676 config: api.ExecConfig{ 677 APIVersion: "client.authentication.k8s.io/v1beta1", 678 InteractiveMode: api.IfAvailableExecInteractiveMode, 679 StdinUnavailable: true, 680 }, 681 isTerminal: true, 682 wantInput: `{ 683 "kind": "ExecCredential", 684 "apiVersion": "client.authentication.k8s.io/v1beta1", 685 "spec": { 686 "interactive": false 687 } 688 }`, 689 output: `{ 690 "kind": "ExecCredential", 691 "apiVersion": "client.authentication.k8s.io/v1beta1", 692 "status": { 693 "token": "foo-bar" 694 } 695 }`, 696 wantCreds: credentials{token: "foo-bar"}, 697 }, 698 { 699 name: "beta-basic-request-with-always-interactive-mode", 700 config: api.ExecConfig{ 701 APIVersion: "client.authentication.k8s.io/v1beta1", 702 InteractiveMode: api.AlwaysExecInteractiveMode, 703 }, 704 wantErr: true, 705 wantErrSubstr: "exec plugin cannot support interactive mode: standard input is not a terminal", 706 }, 707 { 708 name: "beta-basic-request-with-always-interactive-mode-and-terminal-and-stdin-unavailable", 709 config: api.ExecConfig{ 710 APIVersion: "client.authentication.k8s.io/v1beta1", 711 InteractiveMode: api.AlwaysExecInteractiveMode, 712 StdinUnavailable: true, 713 }, 714 isTerminal: true, 715 wantErr: true, 716 wantErrSubstr: "exec plugin cannot support interactive mode: standard input is unavailable", 717 }, 718 { 719 name: "beta-basic-request-with-always-interactive-mode-and-terminal-and-stdin-unavailable-with-message", 720 config: api.ExecConfig{ 721 APIVersion: "client.authentication.k8s.io/v1beta1", 722 InteractiveMode: api.AlwaysExecInteractiveMode, 723 StdinUnavailable: true, 724 StdinUnavailableMessage: "some message", 725 }, 726 isTerminal: true, 727 wantErr: true, 728 wantErrSubstr: "exec plugin cannot support interactive mode: standard input is unavailable: some message", 729 }, 730 { 731 name: "beta-basic-request-with-always-interactive-mode-and-terminal", 732 config: api.ExecConfig{ 733 APIVersion: "client.authentication.k8s.io/v1beta1", 734 InteractiveMode: api.AlwaysExecInteractiveMode, 735 }, 736 isTerminal: true, 737 wantInput: `{ 738 "kind": "ExecCredential", 739 "apiVersion": "client.authentication.k8s.io/v1beta1", 740 "spec": { 741 "interactive": true 742 } 743 }`, 744 output: `{ 745 "kind": "ExecCredential", 746 "apiVersion": "client.authentication.k8s.io/v1beta1", 747 "status": { 748 "token": "foo-bar" 749 } 750 }`, 751 wantCreds: credentials{token: "foo-bar"}, 752 }, 753 { 754 name: "beta-expiry", 755 config: api.ExecConfig{ 756 APIVersion: "client.authentication.k8s.io/v1beta1", 757 InteractiveMode: api.IfAvailableExecInteractiveMode, 758 }, 759 wantInput: `{ 760 "kind": "ExecCredential", 761 "apiVersion": "client.authentication.k8s.io/v1beta1", 762 "spec": { 763 "interactive": false 764 } 765 }`, 766 output: `{ 767 "kind": "ExecCredential", 768 "apiVersion": "client.authentication.k8s.io/v1beta1", 769 "status": { 770 "token": "foo-bar", 771 "expirationTimestamp": "2006-01-02T15:04:05Z" 772 } 773 }`, 774 wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), 775 wantCreds: credentials{token: "foo-bar"}, 776 }, 777 { 778 name: "beta-no-group-version", 779 config: api.ExecConfig{ 780 APIVersion: "client.authentication.k8s.io/v1beta1", 781 InteractiveMode: api.IfAvailableExecInteractiveMode, 782 }, 783 output: `{ 784 "kind": "ExecCredential", 785 "status": { 786 "token": "foo-bar" 787 } 788 }`, 789 wantErr: true, 790 }, 791 { 792 name: "beta-no-status", 793 config: api.ExecConfig{ 794 APIVersion: "client.authentication.k8s.io/v1beta1", 795 InteractiveMode: api.IfAvailableExecInteractiveMode, 796 }, 797 output: `{ 798 "kind": "ExecCredential", 799 "apiVersion":"client.authentication.k8s.io/v1beta1" 800 }`, 801 wantErr: true, 802 }, 803 { 804 name: "beta-no-token", 805 config: api.ExecConfig{ 806 APIVersion: "client.authentication.k8s.io/v1beta1", 807 InteractiveMode: api.IfAvailableExecInteractiveMode, 808 }, 809 output: `{ 810 "kind": "ExecCredential", 811 "apiVersion":"client.authentication.k8s.io/v1beta1", 812 "status": {} 813 }`, 814 wantErr: true, 815 }, 816 { 817 name: "unknown-binary", 818 config: api.ExecConfig{ 819 APIVersion: "client.authentication.k8s.io/v1beta1", 820 Command: "does not exist", 821 InstallHint: "some install hint", 822 InteractiveMode: api.IfAvailableExecInteractiveMode, 823 }, 824 wantErr: true, 825 wantErrSubstr: "some install hint", 826 }, 827 { 828 name: "binary-fails", 829 config: api.ExecConfig{ 830 APIVersion: "client.authentication.k8s.io/v1beta1", 831 InteractiveMode: api.IfAvailableExecInteractiveMode, 832 }, 833 exitCode: 73, 834 wantErr: true, 835 wantErrSubstr: "73", 836 }, 837 { 838 name: "alpha-with-cluster-is-ignored", 839 config: api.ExecConfig{ 840 APIVersion: "client.authentication.k8s.io/v1alpha1", 841 InteractiveMode: api.IfAvailableExecInteractiveMode, 842 }, 843 cluster: &clientauthentication.Cluster{ 844 Server: "foo", 845 TLSServerName: "bar", 846 CertificateAuthorityData: []byte("baz"), 847 Config: &runtime.Unknown{ 848 TypeMeta: runtime.TypeMeta{ 849 APIVersion: "", 850 Kind: "", 851 }, 852 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`), 853 ContentEncoding: "", 854 ContentType: "application/json", 855 }, 856 }, 857 response: &clientauthentication.Response{ 858 Header: map[string][]string{ 859 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 860 }, 861 Code: 401, 862 }, 863 wantInput: `{ 864 "kind":"ExecCredential", 865 "apiVersion":"client.authentication.k8s.io/v1alpha1", 866 "spec": { 867 "response": { 868 "header": { 869 "WWW-Authenticate": [ 870 "Basic realm=\"Access to the staging site\", charset=\"UTF-8\"" 871 ] 872 }, 873 "code": 401 874 } 875 } 876 }`, 877 output: `{ 878 "kind": "ExecCredential", 879 "apiVersion": "client.authentication.k8s.io/v1alpha1", 880 "status": { 881 "token": "foo-bar" 882 } 883 }`, 884 wantCreds: credentials{token: "foo-bar"}, 885 }, 886 { 887 name: "beta-with-cluster-and-provide-cluster-info-is-serialized", 888 config: api.ExecConfig{ 889 APIVersion: "client.authentication.k8s.io/v1beta1", 890 ProvideClusterInfo: true, 891 InteractiveMode: api.IfAvailableExecInteractiveMode, 892 }, 893 cluster: &clientauthentication.Cluster{ 894 Server: "foo", 895 TLSServerName: "bar", 896 CertificateAuthorityData: []byte("baz"), 897 Config: &runtime.Unknown{ 898 TypeMeta: runtime.TypeMeta{ 899 APIVersion: "", 900 Kind: "", 901 }, 902 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 903 ContentEncoding: "", 904 ContentType: "application/json", 905 }, 906 }, 907 response: &clientauthentication.Response{ 908 Header: map[string][]string{ 909 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 910 }, 911 Code: 401, 912 }, 913 wantInput: `{ 914 "kind":"ExecCredential", 915 "apiVersion":"client.authentication.k8s.io/v1beta1", 916 "spec": { 917 "cluster": { 918 "server": "foo", 919 "tls-server-name": "bar", 920 "certificate-authority-data": "YmF6", 921 "config": { 922 "apiVersion": "group/v1", 923 "kind": "PluginConfig", 924 "spec": { 925 "audience": "snorlax" 926 } 927 } 928 }, 929 "interactive": false 930 } 931 }`, 932 output: `{ 933 "kind": "ExecCredential", 934 "apiVersion": "client.authentication.k8s.io/v1beta1", 935 "status": { 936 "token": "foo-bar" 937 } 938 }`, 939 wantCreds: credentials{token: "foo-bar"}, 940 }, 941 { 942 name: "beta-with-cluster-and-without-provide-cluster-info-is-not-serialized", 943 config: api.ExecConfig{ 944 APIVersion: "client.authentication.k8s.io/v1beta1", 945 InteractiveMode: api.IfAvailableExecInteractiveMode, 946 }, 947 cluster: &clientauthentication.Cluster{ 948 Server: "foo", 949 TLSServerName: "bar", 950 CertificateAuthorityData: []byte("baz"), 951 Config: &runtime.Unknown{ 952 TypeMeta: runtime.TypeMeta{ 953 APIVersion: "", 954 Kind: "", 955 }, 956 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 957 ContentEncoding: "", 958 ContentType: "application/json", 959 }, 960 }, 961 response: &clientauthentication.Response{ 962 Header: map[string][]string{ 963 "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, 964 }, 965 Code: 401, 966 }, 967 wantInput: `{ 968 "kind":"ExecCredential", 969 "apiVersion":"client.authentication.k8s.io/v1beta1", 970 "spec": { 971 "interactive": false 972 } 973 }`, 974 output: `{ 975 "kind": "ExecCredential", 976 "apiVersion": "client.authentication.k8s.io/v1beta1", 977 "status": { 978 "token": "foo-bar" 979 } 980 }`, 981 wantCreds: credentials{token: "foo-bar"}, 982 }, 983 { 984 name: "v1-basic-request", 985 config: api.ExecConfig{ 986 APIVersion: "client.authentication.k8s.io/v1", 987 InteractiveMode: api.IfAvailableExecInteractiveMode, 988 }, 989 wantInput: `{ 990 "kind": "ExecCredential", 991 "apiVersion": "client.authentication.k8s.io/v1", 992 "spec": { 993 "interactive": false 994 } 995 }`, 996 output: `{ 997 "kind": "ExecCredential", 998 "apiVersion": "client.authentication.k8s.io/v1", 999 "status": { 1000 "token": "foo-bar" 1001 } 1002 }`, 1003 wantCreds: credentials{token: "foo-bar"}, 1004 }, 1005 { 1006 name: "v1-with-missing-interactive-mode", 1007 config: api.ExecConfig{ 1008 APIVersion: "client.authentication.k8s.io/v1", 1009 }, 1010 wantErr: true, 1011 wantErrSubstr: `exec plugin cannot support interactive mode: unknown interactiveMode: ""`, 1012 }, 1013 } 1014 1015 for _, test := range tests { 1016 t.Run(test.name, func(t *testing.T) { 1017 c := test.config 1018 1019 if c.Command == "" { 1020 c.Command = "./testdata/test-plugin.sh" 1021 c.Env = append(c.Env, api.ExecEnvVar{ 1022 Name: "TEST_OUTPUT", 1023 Value: test.output, 1024 }) 1025 c.Env = append(c.Env, api.ExecEnvVar{ 1026 Name: "TEST_EXIT_CODE", 1027 Value: strconv.Itoa(test.exitCode), 1028 }) 1029 } 1030 1031 a, err := newAuthenticator(newCache(), func(_ int) bool { return test.isTerminal }, &c, test.cluster) 1032 if err != nil { 1033 t.Fatal(err) 1034 } 1035 1036 stderr := &bytes.Buffer{} 1037 a.stderr = stderr 1038 a.environ = func() []string { return nil } 1039 1040 if err := a.refreshCredsLocked(test.response); err != nil { 1041 if !test.wantErr { 1042 t.Errorf("get token %v", err) 1043 } else if !strings.Contains(err.Error(), test.wantErrSubstr) { 1044 t.Errorf("expected error with substring '%v' got '%v'", test.wantErrSubstr, err.Error()) 1045 } 1046 return 1047 } 1048 if test.wantErr { 1049 t.Fatal("expected error getting token") 1050 } 1051 1052 if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) { 1053 t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds) 1054 } 1055 1056 if !a.exp.Equal(test.wantExpiry) { 1057 t.Errorf("expected expiry %v got %v", test.wantExpiry, a.exp) 1058 } 1059 1060 if test.wantInput == "" { 1061 if got := strings.TrimSpace(stderr.String()); got != "" { 1062 t.Errorf("expected no input parameters, got %q", got) 1063 } 1064 return 1065 } 1066 1067 compJSON(t, stderr.Bytes(), []byte(test.wantInput)) 1068 }) 1069 } 1070 } 1071 1072 func TestRoundTripper(t *testing.T) { 1073 wantToken := "" 1074 1075 n := time.Now() 1076 now := func() time.Time { return n } 1077 1078 env := []string{""} 1079 environ := func() []string { 1080 s := make([]string, len(env)) 1081 copy(s, env) 1082 return s 1083 } 1084 1085 setOutput := func(s string) { 1086 env[0] = "TEST_OUTPUT=" + s 1087 } 1088 1089 handler := func(w http.ResponseWriter, r *http.Request) { 1090 gotToken := "" 1091 parts := strings.Split(r.Header.Get("Authorization"), " ") 1092 if len(parts) > 1 && strings.EqualFold(parts[0], "bearer") { 1093 gotToken = parts[1] 1094 } 1095 1096 if wantToken != gotToken { 1097 http.Error(w, "Unauthorized", http.StatusUnauthorized) 1098 return 1099 } 1100 fmt.Fprintln(w, "ok") 1101 } 1102 server := httptest.NewServer(http.HandlerFunc(handler)) 1103 1104 c := api.ExecConfig{ 1105 Command: "./testdata/test-plugin.sh", 1106 APIVersion: "client.authentication.k8s.io/v1alpha1", 1107 InteractiveMode: api.IfAvailableExecInteractiveMode, 1108 } 1109 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil) 1110 if err != nil { 1111 t.Fatal(err) 1112 } 1113 a.environ = environ 1114 a.now = now 1115 a.stderr = ioutil.Discard 1116 1117 tc := &transport.Config{} 1118 if err := a.UpdateTransportConfig(tc); err != nil { 1119 t.Fatal(err) 1120 } 1121 client := http.Client{ 1122 Transport: tc.WrapTransport(http.DefaultTransport), 1123 } 1124 1125 get := func(t *testing.T, statusCode int) { 1126 t.Helper() 1127 resp, err := client.Get(server.URL) 1128 if err != nil { 1129 t.Fatal(err) 1130 } 1131 defer resp.Body.Close() 1132 if resp.StatusCode != statusCode { 1133 t.Errorf("wanted status %d got %d", statusCode, resp.StatusCode) 1134 } 1135 } 1136 1137 setOutput(`{ 1138 "kind": "ExecCredential", 1139 "apiVersion": "client.authentication.k8s.io/v1alpha1", 1140 "status": { 1141 "token": "token1" 1142 } 1143 }`) 1144 wantToken = "token1" 1145 get(t, http.StatusOK) 1146 1147 setOutput(`{ 1148 "kind": "ExecCredential", 1149 "apiVersion": "client.authentication.k8s.io/v1alpha1", 1150 "status": { 1151 "token": "token2" 1152 } 1153 }`) 1154 // Previous token should be cached 1155 get(t, http.StatusOK) 1156 1157 wantToken = "token2" 1158 // Token is still cached, hits unauthorized but causes token to rotate. 1159 get(t, http.StatusUnauthorized) 1160 // Follow up request uses the rotated token. 1161 get(t, http.StatusOK) 1162 1163 setOutput(`{ 1164 "kind": "ExecCredential", 1165 "apiVersion": "client.authentication.k8s.io/v1alpha1", 1166 "status": { 1167 "token": "token3", 1168 "expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `" 1169 } 1170 }`) 1171 wantToken = "token3" 1172 // Token is still cached, hit's unauthorized but causes rotation to token with an expiry. 1173 get(t, http.StatusUnauthorized) 1174 get(t, http.StatusOK) 1175 1176 // Move time forward 2 hours, "token3" is now expired. 1177 n = n.Add(time.Hour * 2) 1178 setOutput(`{ 1179 "kind": "ExecCredential", 1180 "apiVersion": "client.authentication.k8s.io/v1alpha1", 1181 "status": { 1182 "token": "token4", 1183 "expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `" 1184 } 1185 }`) 1186 wantToken = "token4" 1187 // Old token is expired, should refresh automatically without hitting a 401. 1188 get(t, http.StatusOK) 1189 } 1190 1191 func TestAuthorizationHeaderPresentCancelsExecAction(t *testing.T) { 1192 tests := []struct { 1193 name string 1194 setTransportConfig func(*transport.Config) 1195 }{ 1196 { 1197 name: "bearer token", 1198 setTransportConfig: func(config *transport.Config) { 1199 config.BearerToken = "token1f" 1200 }, 1201 }, 1202 { 1203 name: "basic auth", 1204 setTransportConfig: func(config *transport.Config) { 1205 config.Username = "marshmallow" 1206 config.Password = "zelda" 1207 }, 1208 }, 1209 } 1210 for _, test := range tests { 1211 t.Run(test.name, func(t *testing.T) { 1212 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{ 1213 Command: "./testdata/test-plugin.sh", 1214 APIVersion: "client.authentication.k8s.io/v1alpha1", 1215 }, nil) 1216 if err != nil { 1217 t.Fatal(err) 1218 } 1219 1220 // UpdateTransportConfig returns error on existing TLS certificate callback, unless a bearer token is present in the 1221 // transport config, in which case it takes precedence 1222 cert := func() (*tls.Certificate, error) { 1223 return nil, nil 1224 } 1225 tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true, GetCert: cert}} 1226 test.setTransportConfig(tc) 1227 1228 if err := a.UpdateTransportConfig(tc); err != nil { 1229 t.Error("Expected presence of bearer token in config to cancel exec action") 1230 } 1231 }) 1232 } 1233 } 1234 1235 func TestTLSCredentials(t *testing.T) { 1236 now := time.Now() 1237 1238 certPool := x509.NewCertPool() 1239 cert, key := genClientCert(t) 1240 if !certPool.AppendCertsFromPEM(cert) { 1241 t.Fatal("failed to add client cert to CertPool") 1242 } 1243 1244 server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1245 fmt.Fprintln(w, "ok") 1246 })) 1247 server.TLS = &tls.Config{ 1248 ClientAuth: tls.RequireAndVerifyClientCert, 1249 ClientCAs: certPool, 1250 } 1251 server.StartTLS() 1252 defer server.Close() 1253 1254 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{ 1255 Command: "./testdata/test-plugin.sh", 1256 APIVersion: "client.authentication.k8s.io/v1alpha1", 1257 InteractiveMode: api.IfAvailableExecInteractiveMode, 1258 }, nil) 1259 if err != nil { 1260 t.Fatal(err) 1261 } 1262 var output *clientauthentication.ExecCredential 1263 a.environ = func() []string { 1264 data, err := runtime.Encode(codecs.LegacyCodec(a.group), output) 1265 if err != nil { 1266 t.Fatal(err) 1267 } 1268 return []string{"TEST_OUTPUT=" + string(data)} 1269 } 1270 a.now = func() time.Time { return now } 1271 a.stderr = ioutil.Discard 1272 1273 // We're not interested in server's cert, this test is about client cert. 1274 tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true}} 1275 if err := a.UpdateTransportConfig(tc); err != nil { 1276 t.Fatal(err) 1277 } 1278 1279 get := func(t *testing.T, desc string, wantErr bool) { 1280 t.Run(desc, func(t *testing.T) { 1281 tlsCfg, err := transport.TLSConfigFor(tc) 1282 if err != nil { 1283 t.Fatal("TLSConfigFor:", err) 1284 } 1285 client := http.Client{ 1286 Transport: &http.Transport{TLSClientConfig: tlsCfg}, 1287 } 1288 resp, err := client.Get(server.URL) 1289 switch { 1290 case err != nil && !wantErr: 1291 t.Errorf("got client.Get error: %q, want nil", err) 1292 case err == nil && wantErr: 1293 t.Error("got nil client.Get error, want non-nil") 1294 } 1295 if err == nil { 1296 resp.Body.Close() 1297 } 1298 }) 1299 } 1300 1301 output = &clientauthentication.ExecCredential{ 1302 Status: &clientauthentication.ExecCredentialStatus{ 1303 ClientCertificateData: string(cert), 1304 ClientKeyData: string(key), 1305 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 1306 }, 1307 } 1308 get(t, "valid TLS cert", false) 1309 1310 // Advance time to force re-exec. 1311 nCert, nKey := genClientCert(t) 1312 now = now.Add(time.Hour * 2) 1313 output = &clientauthentication.ExecCredential{ 1314 Status: &clientauthentication.ExecCredentialStatus{ 1315 ClientCertificateData: string(nCert), 1316 ClientKeyData: string(nKey), 1317 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 1318 }, 1319 } 1320 get(t, "untrusted TLS cert", true) 1321 1322 now = now.Add(time.Hour * 2) 1323 output = &clientauthentication.ExecCredential{ 1324 Status: &clientauthentication.ExecCredentialStatus{ 1325 ClientCertificateData: string(cert), 1326 ClientKeyData: string(key), 1327 ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, 1328 }, 1329 } 1330 get(t, "valid TLS cert again", false) 1331 } 1332 1333 func TestConcurrentUpdateTransportConfig(t *testing.T) { 1334 n := time.Now() 1335 now := func() time.Time { return n } 1336 1337 env := []string{""} 1338 environ := func() []string { 1339 s := make([]string, len(env)) 1340 copy(s, env) 1341 return s 1342 } 1343 1344 c := api.ExecConfig{ 1345 Command: "./testdata/test-plugin.sh", 1346 APIVersion: "client.authentication.k8s.io/v1alpha1", 1347 } 1348 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil) 1349 if err != nil { 1350 t.Fatal(err) 1351 } 1352 a.environ = environ 1353 a.now = now 1354 a.stderr = ioutil.Discard 1355 1356 stopCh := make(chan struct{}) 1357 defer close(stopCh) 1358 1359 numConcurrent := 2 1360 1361 for i := 0; i < numConcurrent; i++ { 1362 go func() { 1363 for { 1364 tc := &transport.Config{} 1365 a.UpdateTransportConfig(tc) 1366 1367 select { 1368 case <-stopCh: 1369 return 1370 default: 1371 continue 1372 } 1373 } 1374 }() 1375 } 1376 time.Sleep(2 * time.Second) 1377 } 1378 1379 func TestInstallHintRateLimit(t *testing.T) { 1380 tests := []struct { 1381 name string 1382 1383 threshold int 1384 interval time.Duration 1385 1386 calls int 1387 perCallAdvance time.Duration 1388 1389 wantInstallHint int 1390 }{ 1391 { 1392 name: "print-up-to-threshold", 1393 threshold: 2, 1394 interval: time.Second, 1395 calls: 10, 1396 wantInstallHint: 2, 1397 }, 1398 { 1399 name: "after-interval-threshold-resets", 1400 threshold: 2, 1401 interval: time.Second * 5, 1402 calls: 10, 1403 perCallAdvance: time.Second, 1404 wantInstallHint: 4, 1405 }, 1406 } 1407 1408 for _, test := range tests { 1409 t.Run(test.name, func(t *testing.T) { 1410 c := api.ExecConfig{ 1411 Command: "does not exist", 1412 APIVersion: "client.authentication.k8s.io/v1alpha1", 1413 InstallHint: "some install hint", 1414 InteractiveMode: api.IfAvailableExecInteractiveMode, 1415 } 1416 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil) 1417 if err != nil { 1418 t.Fatal(err) 1419 } 1420 1421 a.sometimes.threshold = test.threshold 1422 a.sometimes.interval = test.interval 1423 1424 clock := clock.NewFakeClock(time.Now()) 1425 a.sometimes.clock = clock 1426 1427 count := 0 1428 for i := 0; i < test.calls; i++ { 1429 err := a.refreshCredsLocked(&clientauthentication.Response{}) 1430 if strings.Contains(err.Error(), c.InstallHint) { 1431 count++ 1432 } 1433 1434 clock.SetTime(clock.Now().Add(test.perCallAdvance)) 1435 } 1436 1437 if test.wantInstallHint != count { 1438 t.Errorf( 1439 "%s: expected install hint %d times got %d", 1440 test.name, 1441 test.wantInstallHint, 1442 count, 1443 ) 1444 } 1445 }) 1446 } 1447 } 1448 1449 // genClientCert generates an x509 certificate for testing. Certificate and key 1450 // are returned in PEM encoding. The generated cert expires in 24 hours. 1451 func genClientCert(t *testing.T) ([]byte, []byte) { 1452 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 1453 if err != nil { 1454 t.Fatal(err) 1455 } 1456 keyRaw, err := x509.MarshalECPrivateKey(key) 1457 if err != nil { 1458 t.Fatal(err) 1459 } 1460 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 1461 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 1462 if err != nil { 1463 t.Fatal(err) 1464 } 1465 cert := &x509.Certificate{ 1466 SerialNumber: serialNumber, 1467 Subject: pkix.Name{Organization: []string{"Acme Co"}}, 1468 NotBefore: time.Now(), 1469 NotAfter: time.Now().Add(24 * time.Hour), 1470 1471 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 1472 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 1473 BasicConstraintsValid: true, 1474 } 1475 certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) 1476 if err != nil { 1477 t.Fatal(err) 1478 } 1479 return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}), 1480 pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw}) 1481 }