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  }