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