k8s.io/client-go@v0.31.1/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" 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/client-go/pkg/apis/clientauthentication" 43 "k8s.io/client-go/tools/clientcmd/api" 44 "k8s.io/client-go/transport" 45 testingclock "k8s.io/utils/clock/testing" 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/v1beta1", 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/v1beta1", 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/v1beta1", 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/v1beta1", 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/v1beta1", 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/v1betaa1", 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/v1beta1", 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 wantInput string 317 wantCreds credentials 318 wantExpiry time.Time 319 wantErr bool 320 wantErrSubstr string 321 }{ 322 { 323 name: "beta-with-TLS-credentials", 324 config: api.ExecConfig{ 325 APIVersion: "client.authentication.k8s.io/v1beta1", 326 InteractiveMode: api.IfAvailableExecInteractiveMode, 327 }, 328 wantInput: `{ 329 "kind":"ExecCredential", 330 "apiVersion":"client.authentication.k8s.io/v1beta1", 331 "spec": { 332 "interactive": false 333 } 334 }`, 335 output: fmt.Sprintf(`{ 336 "kind": "ExecCredential", 337 "apiVersion": "client.authentication.k8s.io/v1beta1", 338 "status": { 339 "clientKeyData": %q, 340 "clientCertificateData": %q 341 } 342 }`, keyData, certData), 343 wantCreds: credentials{cert: validCert}, 344 }, 345 { 346 name: "beta-with-bad-TLS-credentials", 347 config: api.ExecConfig{ 348 APIVersion: "client.authentication.k8s.io/v1beta1", 349 InteractiveMode: api.IfAvailableExecInteractiveMode, 350 }, 351 output: `{ 352 "kind": "ExecCredential", 353 "apiVersion": "client.authentication.k8s.io/v1beta1", 354 "status": { 355 "clientKeyData": "foo", 356 "clientCertificateData": "bar" 357 } 358 }`, 359 wantErr: true, 360 }, 361 { 362 name: "beta-cert-but-no-key", 363 config: api.ExecConfig{ 364 APIVersion: "client.authentication.k8s.io/v1beta1", 365 InteractiveMode: api.IfAvailableExecInteractiveMode, 366 }, 367 output: fmt.Sprintf(`{ 368 "kind": "ExecCredential", 369 "apiVersion": "client.authentication.k8s.io/v1beta1", 370 "status": { 371 "clientCertificateData": %q 372 } 373 }`, certData), 374 wantErr: true, 375 }, 376 { 377 name: "beta-basic-request", 378 config: api.ExecConfig{ 379 APIVersion: "client.authentication.k8s.io/v1beta1", 380 InteractiveMode: api.IfAvailableExecInteractiveMode, 381 }, 382 wantInput: `{ 383 "kind": "ExecCredential", 384 "apiVersion": "client.authentication.k8s.io/v1beta1", 385 "spec": { 386 "interactive": false 387 } 388 }`, 389 output: `{ 390 "kind": "ExecCredential", 391 "apiVersion": "client.authentication.k8s.io/v1beta1", 392 "status": { 393 "token": "foo-bar" 394 } 395 }`, 396 wantCreds: credentials{token: "foo-bar"}, 397 }, 398 { 399 name: "beta-basic-request-with-never-interactive-mode", 400 config: api.ExecConfig{ 401 APIVersion: "client.authentication.k8s.io/v1beta1", 402 InteractiveMode: api.NeverExecInteractiveMode, 403 }, 404 wantInput: `{ 405 "kind": "ExecCredential", 406 "apiVersion": "client.authentication.k8s.io/v1beta1", 407 "spec": { 408 "interactive": false 409 } 410 }`, 411 output: `{ 412 "kind": "ExecCredential", 413 "apiVersion": "client.authentication.k8s.io/v1beta1", 414 "status": { 415 "token": "foo-bar" 416 } 417 }`, 418 wantCreds: credentials{token: "foo-bar"}, 419 }, 420 { 421 name: "beta-basic-request-with-never-interactive-mode-and-stdin-unavailable", 422 config: api.ExecConfig{ 423 APIVersion: "client.authentication.k8s.io/v1beta1", 424 InteractiveMode: api.NeverExecInteractiveMode, 425 StdinUnavailable: true, 426 }, 427 wantInput: `{ 428 "kind": "ExecCredential", 429 "apiVersion": "client.authentication.k8s.io/v1beta1", 430 "spec": { 431 "interactive": false 432 } 433 }`, 434 output: `{ 435 "kind": "ExecCredential", 436 "apiVersion": "client.authentication.k8s.io/v1beta1", 437 "status": { 438 "token": "foo-bar" 439 } 440 }`, 441 wantCreds: credentials{token: "foo-bar"}, 442 }, 443 { 444 name: "beta-basic-request-with-if-available-interactive-mode", 445 config: api.ExecConfig{ 446 APIVersion: "client.authentication.k8s.io/v1beta1", 447 InteractiveMode: api.IfAvailableExecInteractiveMode, 448 }, 449 wantInput: `{ 450 "kind": "ExecCredential", 451 "apiVersion": "client.authentication.k8s.io/v1beta1", 452 "spec": { 453 "interactive": false 454 } 455 }`, 456 output: `{ 457 "kind": "ExecCredential", 458 "apiVersion": "client.authentication.k8s.io/v1beta1", 459 "status": { 460 "token": "foo-bar" 461 } 462 }`, 463 wantCreds: credentials{token: "foo-bar"}, 464 }, 465 { 466 name: "beta-basic-request-with-if-available-interactive-mode-and-stdin-unavailable", 467 config: api.ExecConfig{ 468 APIVersion: "client.authentication.k8s.io/v1beta1", 469 InteractiveMode: api.IfAvailableExecInteractiveMode, 470 StdinUnavailable: true, 471 }, 472 wantInput: `{ 473 "kind": "ExecCredential", 474 "apiVersion": "client.authentication.k8s.io/v1beta1", 475 "spec": { 476 "interactive": false 477 } 478 }`, 479 output: `{ 480 "kind": "ExecCredential", 481 "apiVersion": "client.authentication.k8s.io/v1beta1", 482 "status": { 483 "token": "foo-bar" 484 } 485 }`, 486 wantCreds: credentials{token: "foo-bar"}, 487 }, 488 { 489 name: "beta-basic-request-with-if-available-interactive-mode-and-terminal", 490 config: api.ExecConfig{ 491 APIVersion: "client.authentication.k8s.io/v1beta1", 492 InteractiveMode: api.IfAvailableExecInteractiveMode, 493 }, 494 isTerminal: true, 495 wantInput: `{ 496 "kind": "ExecCredential", 497 "apiVersion": "client.authentication.k8s.io/v1beta1", 498 "spec": { 499 "interactive": true 500 } 501 }`, 502 output: `{ 503 "kind": "ExecCredential", 504 "apiVersion": "client.authentication.k8s.io/v1beta1", 505 "status": { 506 "token": "foo-bar" 507 } 508 }`, 509 wantCreds: credentials{token: "foo-bar"}, 510 }, 511 { 512 name: "beta-basic-request-with-if-available-interactive-mode-and-terminal-and-stdin-unavailable", 513 config: api.ExecConfig{ 514 APIVersion: "client.authentication.k8s.io/v1beta1", 515 InteractiveMode: api.IfAvailableExecInteractiveMode, 516 StdinUnavailable: true, 517 }, 518 isTerminal: true, 519 wantInput: `{ 520 "kind": "ExecCredential", 521 "apiVersion": "client.authentication.k8s.io/v1beta1", 522 "spec": { 523 "interactive": false 524 } 525 }`, 526 output: `{ 527 "kind": "ExecCredential", 528 "apiVersion": "client.authentication.k8s.io/v1beta1", 529 "status": { 530 "token": "foo-bar" 531 } 532 }`, 533 wantCreds: credentials{token: "foo-bar"}, 534 }, 535 { 536 name: "beta-basic-request-with-always-interactive-mode", 537 config: api.ExecConfig{ 538 APIVersion: "client.authentication.k8s.io/v1beta1", 539 InteractiveMode: api.AlwaysExecInteractiveMode, 540 }, 541 wantErr: true, 542 wantErrSubstr: "exec plugin cannot support interactive mode: standard input is not a terminal", 543 }, 544 { 545 name: "beta-basic-request-with-always-interactive-mode-and-terminal-and-stdin-unavailable", 546 config: api.ExecConfig{ 547 APIVersion: "client.authentication.k8s.io/v1beta1", 548 InteractiveMode: api.AlwaysExecInteractiveMode, 549 StdinUnavailable: true, 550 }, 551 isTerminal: true, 552 wantErr: true, 553 wantErrSubstr: "exec plugin cannot support interactive mode: standard input is unavailable", 554 }, 555 { 556 name: "beta-basic-request-with-always-interactive-mode-and-terminal-and-stdin-unavailable-with-message", 557 config: api.ExecConfig{ 558 APIVersion: "client.authentication.k8s.io/v1beta1", 559 InteractiveMode: api.AlwaysExecInteractiveMode, 560 StdinUnavailable: true, 561 StdinUnavailableMessage: "some message", 562 }, 563 isTerminal: true, 564 wantErr: true, 565 wantErrSubstr: "exec plugin cannot support interactive mode: standard input is unavailable: some message", 566 }, 567 { 568 name: "beta-basic-request-with-always-interactive-mode-and-terminal", 569 config: api.ExecConfig{ 570 APIVersion: "client.authentication.k8s.io/v1beta1", 571 InteractiveMode: api.AlwaysExecInteractiveMode, 572 }, 573 isTerminal: true, 574 wantInput: `{ 575 "kind": "ExecCredential", 576 "apiVersion": "client.authentication.k8s.io/v1beta1", 577 "spec": { 578 "interactive": true 579 } 580 }`, 581 output: `{ 582 "kind": "ExecCredential", 583 "apiVersion": "client.authentication.k8s.io/v1beta1", 584 "status": { 585 "token": "foo-bar" 586 } 587 }`, 588 wantCreds: credentials{token: "foo-bar"}, 589 }, 590 { 591 name: "beta-expiry", 592 config: api.ExecConfig{ 593 APIVersion: "client.authentication.k8s.io/v1beta1", 594 InteractiveMode: api.IfAvailableExecInteractiveMode, 595 }, 596 wantInput: `{ 597 "kind": "ExecCredential", 598 "apiVersion": "client.authentication.k8s.io/v1beta1", 599 "spec": { 600 "interactive": false 601 } 602 }`, 603 output: `{ 604 "kind": "ExecCredential", 605 "apiVersion": "client.authentication.k8s.io/v1beta1", 606 "status": { 607 "token": "foo-bar", 608 "expirationTimestamp": "2006-01-02T15:04:05Z" 609 } 610 }`, 611 wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), 612 wantCreds: credentials{token: "foo-bar"}, 613 }, 614 { 615 name: "beta-no-group-version", 616 config: api.ExecConfig{ 617 APIVersion: "client.authentication.k8s.io/v1beta1", 618 InteractiveMode: api.IfAvailableExecInteractiveMode, 619 }, 620 output: `{ 621 "kind": "ExecCredential", 622 "status": { 623 "token": "foo-bar" 624 } 625 }`, 626 wantErr: true, 627 }, 628 { 629 name: "beta-no-status", 630 config: api.ExecConfig{ 631 APIVersion: "client.authentication.k8s.io/v1beta1", 632 InteractiveMode: api.IfAvailableExecInteractiveMode, 633 }, 634 output: `{ 635 "kind": "ExecCredential", 636 "apiVersion":"client.authentication.k8s.io/v1beta1" 637 }`, 638 wantErr: true, 639 }, 640 { 641 name: "beta-no-token", 642 config: api.ExecConfig{ 643 APIVersion: "client.authentication.k8s.io/v1beta1", 644 InteractiveMode: api.IfAvailableExecInteractiveMode, 645 }, 646 output: `{ 647 "kind": "ExecCredential", 648 "apiVersion":"client.authentication.k8s.io/v1beta1", 649 "status": {} 650 }`, 651 wantErr: true, 652 }, 653 { 654 name: "unknown-binary", 655 config: api.ExecConfig{ 656 APIVersion: "client.authentication.k8s.io/v1beta1", 657 Command: "does not exist", 658 InstallHint: "some install hint", 659 InteractiveMode: api.IfAvailableExecInteractiveMode, 660 }, 661 wantErr: true, 662 wantErrSubstr: "some install hint", 663 }, 664 { 665 name: "binary-fails", 666 config: api.ExecConfig{ 667 APIVersion: "client.authentication.k8s.io/v1beta1", 668 InteractiveMode: api.IfAvailableExecInteractiveMode, 669 }, 670 exitCode: 73, 671 wantErr: true, 672 wantErrSubstr: "73", 673 }, 674 { 675 name: "beta-with-cluster-and-provide-cluster-info-is-serialized", 676 config: api.ExecConfig{ 677 APIVersion: "client.authentication.k8s.io/v1beta1", 678 ProvideClusterInfo: true, 679 InteractiveMode: api.IfAvailableExecInteractiveMode, 680 }, 681 cluster: &clientauthentication.Cluster{ 682 Server: "foo", 683 TLSServerName: "bar", 684 CertificateAuthorityData: []byte("baz"), 685 Config: &runtime.Unknown{ 686 TypeMeta: runtime.TypeMeta{ 687 APIVersion: "", 688 Kind: "", 689 }, 690 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 691 ContentEncoding: "", 692 ContentType: "application/json", 693 }, 694 }, 695 wantInput: `{ 696 "kind":"ExecCredential", 697 "apiVersion":"client.authentication.k8s.io/v1beta1", 698 "spec": { 699 "cluster": { 700 "server": "foo", 701 "tls-server-name": "bar", 702 "certificate-authority-data": "YmF6", 703 "config": { 704 "apiVersion": "group/v1", 705 "kind": "PluginConfig", 706 "spec": { 707 "audience": "snorlax" 708 } 709 } 710 }, 711 "interactive": false 712 } 713 }`, 714 output: `{ 715 "kind": "ExecCredential", 716 "apiVersion": "client.authentication.k8s.io/v1beta1", 717 "status": { 718 "token": "foo-bar" 719 } 720 }`, 721 wantCreds: credentials{token: "foo-bar"}, 722 }, 723 { 724 name: "beta-with-cluster-and-without-provide-cluster-info-is-not-serialized", 725 config: api.ExecConfig{ 726 APIVersion: "client.authentication.k8s.io/v1beta1", 727 InteractiveMode: api.IfAvailableExecInteractiveMode, 728 }, 729 cluster: &clientauthentication.Cluster{ 730 Server: "foo", 731 TLSServerName: "bar", 732 CertificateAuthorityData: []byte("baz"), 733 Config: &runtime.Unknown{ 734 TypeMeta: runtime.TypeMeta{ 735 APIVersion: "", 736 Kind: "", 737 }, 738 Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), 739 ContentEncoding: "", 740 ContentType: "application/json", 741 }, 742 }, 743 wantInput: `{ 744 "kind":"ExecCredential", 745 "apiVersion":"client.authentication.k8s.io/v1beta1", 746 "spec": { 747 "interactive": false 748 } 749 }`, 750 output: `{ 751 "kind": "ExecCredential", 752 "apiVersion": "client.authentication.k8s.io/v1beta1", 753 "status": { 754 "token": "foo-bar" 755 } 756 }`, 757 wantCreds: credentials{token: "foo-bar"}, 758 }, 759 { 760 name: "v1-basic-request", 761 config: api.ExecConfig{ 762 APIVersion: "client.authentication.k8s.io/v1", 763 InteractiveMode: api.IfAvailableExecInteractiveMode, 764 }, 765 wantInput: `{ 766 "kind": "ExecCredential", 767 "apiVersion": "client.authentication.k8s.io/v1", 768 "spec": { 769 "interactive": false 770 } 771 }`, 772 output: `{ 773 "kind": "ExecCredential", 774 "apiVersion": "client.authentication.k8s.io/v1", 775 "status": { 776 "token": "foo-bar" 777 } 778 }`, 779 wantCreds: credentials{token: "foo-bar"}, 780 }, 781 { 782 name: "v1-with-missing-interactive-mode", 783 config: api.ExecConfig{ 784 APIVersion: "client.authentication.k8s.io/v1", 785 }, 786 wantErr: true, 787 wantErrSubstr: `exec plugin cannot support interactive mode: unknown interactiveMode: ""`, 788 }, 789 } 790 791 for _, test := range tests { 792 t.Run(test.name, func(t *testing.T) { 793 c := test.config 794 795 if c.Command == "" { 796 c.Command = "./testdata/test-plugin.sh" 797 c.Env = append(c.Env, api.ExecEnvVar{ 798 Name: "TEST_OUTPUT", 799 Value: test.output, 800 }) 801 c.Env = append(c.Env, api.ExecEnvVar{ 802 Name: "TEST_EXIT_CODE", 803 Value: strconv.Itoa(test.exitCode), 804 }) 805 } 806 807 a, err := newAuthenticator(newCache(), func(_ int) bool { return test.isTerminal }, &c, test.cluster) 808 if err != nil { 809 t.Fatal(err) 810 } 811 812 stderr := &bytes.Buffer{} 813 a.stderr = stderr 814 a.environ = func() []string { return nil } 815 816 if err := a.refreshCredsLocked(); err != nil { 817 if !test.wantErr { 818 t.Errorf("get token %v", err) 819 } else if !strings.Contains(err.Error(), test.wantErrSubstr) { 820 t.Errorf("expected error with substring '%v' got '%v'", test.wantErrSubstr, err.Error()) 821 } 822 return 823 } 824 if test.wantErr { 825 t.Fatal("expected error getting token") 826 } 827 828 if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) { 829 t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds) 830 } 831 832 if !a.exp.Equal(test.wantExpiry) { 833 t.Errorf("expected expiry %v got %v", test.wantExpiry, a.exp) 834 } 835 836 if test.wantInput == "" { 837 if got := strings.TrimSpace(stderr.String()); got != "" { 838 t.Errorf("expected no input parameters, got %q", got) 839 } 840 return 841 } 842 843 compJSON(t, stderr.Bytes(), []byte(test.wantInput)) 844 }) 845 } 846 } 847 848 func TestRoundTripper(t *testing.T) { 849 wantToken := "" 850 851 n := time.Now() 852 now := func() time.Time { return n } 853 854 env := []string{""} 855 environ := func() []string { 856 s := make([]string, len(env)) 857 copy(s, env) 858 return s 859 } 860 861 setOutput := func(s string) { 862 env[0] = "TEST_OUTPUT=" + s 863 } 864 865 handler := func(w http.ResponseWriter, r *http.Request) { 866 gotToken := "" 867 parts := strings.Split(r.Header.Get("Authorization"), " ") 868 if len(parts) > 1 && strings.EqualFold(parts[0], "bearer") { 869 gotToken = parts[1] 870 } 871 872 if wantToken != gotToken { 873 http.Error(w, "Unauthorized", http.StatusUnauthorized) 874 return 875 } 876 fmt.Fprintln(w, "ok") 877 } 878 server := httptest.NewServer(http.HandlerFunc(handler)) 879 880 c := api.ExecConfig{ 881 Command: "./testdata/test-plugin.sh", 882 APIVersion: "client.authentication.k8s.io/v1beta1", 883 InteractiveMode: api.IfAvailableExecInteractiveMode, 884 } 885 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil) 886 if err != nil { 887 t.Fatal(err) 888 } 889 a.environ = environ 890 a.now = now 891 a.stderr = io.Discard 892 893 tc := &transport.Config{} 894 if err := a.UpdateTransportConfig(tc); err != nil { 895 t.Fatal(err) 896 } 897 client := http.Client{ 898 Transport: tc.WrapTransport(http.DefaultTransport), 899 } 900 901 get := func(t *testing.T, statusCode int) { 902 t.Helper() 903 resp, err := client.Get(server.URL) 904 if err != nil { 905 t.Fatal(err) 906 } 907 defer resp.Body.Close() 908 if resp.StatusCode != statusCode { 909 t.Errorf("wanted status %d got %d", statusCode, resp.StatusCode) 910 } 911 } 912 913 setOutput(`{ 914 "kind": "ExecCredential", 915 "apiVersion": "client.authentication.k8s.io/v1beta1", 916 "status": { 917 "token": "token1" 918 } 919 }`) 920 wantToken = "token1" 921 get(t, http.StatusOK) 922 923 setOutput(`{ 924 "kind": "ExecCredential", 925 "apiVersion": "client.authentication.k8s.io/v1beta1", 926 "status": { 927 "token": "token2" 928 } 929 }`) 930 // Previous token should be cached 931 get(t, http.StatusOK) 932 933 wantToken = "token2" 934 // Token is still cached, hits unauthorized but causes token to rotate. 935 get(t, http.StatusUnauthorized) 936 // Follow up request uses the rotated token. 937 get(t, http.StatusOK) 938 939 setOutput(`{ 940 "kind": "ExecCredential", 941 "apiVersion": "client.authentication.k8s.io/v1beta1", 942 "status": { 943 "token": "token3", 944 "expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `" 945 } 946 }`) 947 wantToken = "token3" 948 // Token is still cached, hit's unauthorized but causes rotation to token with an expiry. 949 get(t, http.StatusUnauthorized) 950 get(t, http.StatusOK) 951 952 // Move time forward 2 hours, "token3" is now expired. 953 n = n.Add(time.Hour * 2) 954 setOutput(`{ 955 "kind": "ExecCredential", 956 "apiVersion": "client.authentication.k8s.io/v1beta1", 957 "status": { 958 "token": "token4", 959 "expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `" 960 } 961 }`) 962 wantToken = "token4" 963 // Old token is expired, should refresh automatically without hitting a 401. 964 get(t, http.StatusOK) 965 } 966 967 func TestAuthorizationHeaderPresentCancelsExecAction(t *testing.T) { 968 tests := []struct { 969 name string 970 setTransportConfig func(*transport.Config) 971 }{ 972 { 973 name: "bearer token", 974 setTransportConfig: func(config *transport.Config) { 975 config.BearerToken = "token1f" 976 }, 977 }, 978 { 979 name: "basic auth", 980 setTransportConfig: func(config *transport.Config) { 981 config.Username = "marshmallow" 982 config.Password = "zelda" 983 }, 984 }, 985 { 986 name: "cert auth", 987 setTransportConfig: func(config *transport.Config) { 988 config.TLS.CertData = []byte("some-cert-data") 989 config.TLS.KeyData = []byte("some-key-data") 990 }, 991 }, 992 } 993 for _, test := range tests { 994 t.Run(test.name, func(t *testing.T) { 995 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{ 996 Command: "./testdata/test-plugin.sh", 997 APIVersion: "client.authentication.k8s.io/v1beta1", 998 }, nil) 999 if err != nil { 1000 t.Fatal(err) 1001 } 1002 1003 // UpdateTransportConfig returns error on existing TLS certificate callback, unless a bearer token is present in the 1004 // transport config, in which case it takes precedence 1005 cert := func() (*tls.Certificate, error) { 1006 return nil, nil 1007 } 1008 tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true, GetCertHolder: &transport.GetCertHolder{GetCert: cert}}} 1009 test.setTransportConfig(tc) 1010 1011 if err := a.UpdateTransportConfig(tc); err != nil { 1012 t.Error("Expected presence of bearer token in config to cancel exec action") 1013 } 1014 }) 1015 } 1016 } 1017 1018 func TestTLSCredentials(t *testing.T) { 1019 now := time.Now() 1020 1021 certPool := x509.NewCertPool() 1022 cert, key := genClientCert(t) 1023 if !certPool.AppendCertsFromPEM(cert) { 1024 t.Fatal("failed to add client cert to CertPool") 1025 } 1026 1027 server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1028 fmt.Fprintln(w, "ok") 1029 })) 1030 server.TLS = &tls.Config{ 1031 ClientAuth: tls.RequireAndVerifyClientCert, 1032 ClientCAs: certPool, 1033 } 1034 server.StartTLS() 1035 defer server.Close() 1036 1037 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &api.ExecConfig{ 1038 Command: "./testdata/test-plugin.sh", 1039 APIVersion: "client.authentication.k8s.io/v1beta1", 1040 InteractiveMode: api.IfAvailableExecInteractiveMode, 1041 }, nil) 1042 if err != nil { 1043 t.Fatal(err) 1044 } 1045 var output *clientauthentication.ExecCredential 1046 a.environ = func() []string { 1047 data, err := runtime.Encode(codecs.LegacyCodec(a.group), output) 1048 if err != nil { 1049 t.Fatal(err) 1050 } 1051 return []string{"TEST_OUTPUT=" + string(data)} 1052 } 1053 a.now = func() time.Time { return now } 1054 a.stderr = io.Discard 1055 1056 // We're not interested in server's cert, this test is about client cert. 1057 tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true}} 1058 if err := a.UpdateTransportConfig(tc); err != nil { 1059 t.Fatal(err) 1060 } 1061 1062 get := func(t *testing.T, desc string, wantErr bool) { 1063 t.Run(desc, func(t *testing.T) { 1064 tlsCfg, err := transport.TLSConfigFor(tc) 1065 if err != nil { 1066 t.Fatal("TLSConfigFor:", err) 1067 } 1068 client := http.Client{ 1069 Transport: &http.Transport{TLSClientConfig: tlsCfg}, 1070 } 1071 resp, err := client.Get(server.URL) 1072 switch { 1073 case err != nil && !wantErr: 1074 t.Errorf("got client.Get error: %q, want nil", err) 1075 case err == nil && wantErr: 1076 t.Error("got nil client.Get error, want non-nil") 1077 } 1078 if err == nil { 1079 resp.Body.Close() 1080 } 1081 }) 1082 } 1083 1084 output = &clientauthentication.ExecCredential{ 1085 Status: &clientauthentication.ExecCredentialStatus{ 1086 ClientCertificateData: string(cert), 1087 ClientKeyData: string(key), 1088 ExpirationTimestamp: &v1.Time{Time: now.Add(time.Hour)}, 1089 }, 1090 } 1091 get(t, "valid TLS cert", false) 1092 1093 // Advance time to force re-exec. 1094 nCert, nKey := genClientCert(t) 1095 now = now.Add(time.Hour * 2) 1096 output = &clientauthentication.ExecCredential{ 1097 Status: &clientauthentication.ExecCredentialStatus{ 1098 ClientCertificateData: string(nCert), 1099 ClientKeyData: string(nKey), 1100 ExpirationTimestamp: &v1.Time{Time: now.Add(time.Hour)}, 1101 }, 1102 } 1103 get(t, "untrusted TLS cert", true) 1104 1105 now = now.Add(time.Hour * 2) 1106 output = &clientauthentication.ExecCredential{ 1107 Status: &clientauthentication.ExecCredentialStatus{ 1108 ClientCertificateData: string(cert), 1109 ClientKeyData: string(key), 1110 ExpirationTimestamp: &v1.Time{Time: now.Add(time.Hour)}, 1111 }, 1112 } 1113 get(t, "valid TLS cert again", false) 1114 } 1115 1116 func TestConcurrentUpdateTransportConfig(t *testing.T) { 1117 n := time.Now() 1118 now := func() time.Time { return n } 1119 1120 env := []string{""} 1121 environ := func() []string { 1122 s := make([]string, len(env)) 1123 copy(s, env) 1124 return s 1125 } 1126 1127 c := api.ExecConfig{ 1128 Command: "./testdata/test-plugin.sh", 1129 APIVersion: "client.authentication.k8s.io/v1beta1", 1130 } 1131 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil) 1132 if err != nil { 1133 t.Fatal(err) 1134 } 1135 a.environ = environ 1136 a.now = now 1137 a.stderr = io.Discard 1138 1139 stopCh := make(chan struct{}) 1140 defer close(stopCh) 1141 1142 numConcurrent := 2 1143 1144 for i := 0; i < numConcurrent; i++ { 1145 go func() { 1146 for { 1147 tc := &transport.Config{} 1148 a.UpdateTransportConfig(tc) 1149 1150 select { 1151 case <-stopCh: 1152 return 1153 default: 1154 continue 1155 } 1156 } 1157 }() 1158 } 1159 time.Sleep(2 * time.Second) 1160 } 1161 1162 func TestInstallHintRateLimit(t *testing.T) { 1163 tests := []struct { 1164 name string 1165 1166 threshold int 1167 interval time.Duration 1168 1169 calls int 1170 perCallAdvance time.Duration 1171 1172 wantInstallHint int 1173 }{ 1174 { 1175 name: "print-up-to-threshold", 1176 threshold: 2, 1177 interval: time.Second, 1178 calls: 10, 1179 wantInstallHint: 2, 1180 }, 1181 { 1182 name: "after-interval-threshold-resets", 1183 threshold: 2, 1184 interval: time.Second * 5, 1185 calls: 10, 1186 perCallAdvance: time.Second, 1187 wantInstallHint: 4, 1188 }, 1189 } 1190 1191 for _, test := range tests { 1192 t.Run(test.name, func(t *testing.T) { 1193 c := api.ExecConfig{ 1194 Command: "does not exist", 1195 APIVersion: "client.authentication.k8s.io/v1beta1", 1196 InstallHint: "some install hint", 1197 InteractiveMode: api.IfAvailableExecInteractiveMode, 1198 } 1199 a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil) 1200 if err != nil { 1201 t.Fatal(err) 1202 } 1203 1204 a.sometimes.threshold = test.threshold 1205 a.sometimes.interval = test.interval 1206 1207 clock := testingclock.NewFakeClock(time.Now()) 1208 a.sometimes.clock = clock 1209 1210 count := 0 1211 for i := 0; i < test.calls; i++ { 1212 err := a.refreshCredsLocked() 1213 if strings.Contains(err.Error(), c.InstallHint) { 1214 count++ 1215 } 1216 1217 clock.SetTime(clock.Now().Add(test.perCallAdvance)) 1218 } 1219 1220 if test.wantInstallHint != count { 1221 t.Errorf( 1222 "%s: expected install hint %d times got %d", 1223 test.name, 1224 test.wantInstallHint, 1225 count, 1226 ) 1227 } 1228 }) 1229 } 1230 } 1231 1232 // genClientCert generates an x509 certificate for testing. Certificate and key 1233 // are returned in PEM encoding. The generated cert expires in 24 hours. 1234 func genClientCert(t *testing.T) ([]byte, []byte) { 1235 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 1236 if err != nil { 1237 t.Fatal(err) 1238 } 1239 keyRaw, err := x509.MarshalECPrivateKey(key) 1240 if err != nil { 1241 t.Fatal(err) 1242 } 1243 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 1244 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 1245 if err != nil { 1246 t.Fatal(err) 1247 } 1248 cert := &x509.Certificate{ 1249 SerialNumber: serialNumber, 1250 Subject: pkix.Name{Organization: []string{"Acme Co"}}, 1251 NotBefore: time.Now(), 1252 NotAfter: time.Now().Add(24 * time.Hour), 1253 1254 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 1255 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 1256 BasicConstraintsValid: true, 1257 } 1258 certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) 1259 if err != nil { 1260 t.Fatal(err) 1261 } 1262 return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}), 1263 pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw}) 1264 }