mirror of
https://github.com/kubernetes/client-go.git
synced 2025-09-29 08:26:24 +00:00
795 lines
20 KiB
Go
795 lines
20 KiB
Go
/*
|
|
Copyright 2018 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package exec
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/pkg/apis/clientauthentication"
|
|
"k8s.io/client-go/tools/clientcmd/api"
|
|
"k8s.io/client-go/transport"
|
|
)
|
|
|
|
var (
|
|
certData = []byte(`-----BEGIN CERTIFICATE-----
|
|
MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu
|
|
MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz
|
|
MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB
|
|
BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v
|
|
b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj
|
|
lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2
|
|
I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb
|
|
1Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F
|
|
kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P
|
|
AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ
|
|
KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/
|
|
p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3
|
|
jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq
|
|
6GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ
|
|
HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ
|
|
BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0
|
|
-----END CERTIFICATE-----`)
|
|
keyData = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i
|
|
wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc
|
|
kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG
|
|
0hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv
|
|
RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi
|
|
ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU
|
|
FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK
|
|
aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm
|
|
5mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M
|
|
ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0
|
|
JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr
|
|
7L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI
|
|
cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey
|
|
OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/
|
|
rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9
|
|
8PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg
|
|
nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k
|
|
2bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII
|
|
NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+
|
|
GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S
|
|
3OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG
|
|
77uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/
|
|
bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/
|
|
F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX
|
|
stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa
|
|
-----END RSA PRIVATE KEY-----`)
|
|
validCert *tls.Certificate
|
|
)
|
|
|
|
func init() {
|
|
cert, err := tls.X509KeyPair(certData, keyData)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
validCert = &cert
|
|
}
|
|
|
|
func TestCacheKey(t *testing.T) {
|
|
c1 := &api.ExecConfig{
|
|
Command: "foo-bar",
|
|
Args: []string{"1", "2"},
|
|
Env: []api.ExecEnvVar{
|
|
{Name: "3", Value: "4"},
|
|
{Name: "5", Value: "6"},
|
|
{Name: "7", Value: "8"},
|
|
},
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
}
|
|
c2 := &api.ExecConfig{
|
|
Command: "foo-bar",
|
|
Args: []string{"1", "2"},
|
|
Env: []api.ExecEnvVar{
|
|
{Name: "3", Value: "4"},
|
|
{Name: "5", Value: "6"},
|
|
{Name: "7", Value: "8"},
|
|
},
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
}
|
|
c3 := &api.ExecConfig{
|
|
Command: "foo-bar",
|
|
Args: []string{"1", "2"},
|
|
Env: []api.ExecEnvVar{
|
|
{Name: "3", Value: "4"},
|
|
{Name: "5", Value: "6"},
|
|
},
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
}
|
|
key1 := cacheKey(c1)
|
|
key2 := cacheKey(c2)
|
|
key3 := cacheKey(c3)
|
|
if key1 != key2 {
|
|
t.Error("key1 and key2 didn't match")
|
|
}
|
|
if key1 == key3 {
|
|
t.Error("key1 and key3 matched")
|
|
}
|
|
if key2 == key3 {
|
|
t.Error("key2 and key3 matched")
|
|
}
|
|
}
|
|
|
|
func compJSON(t *testing.T, got, want []byte) {
|
|
t.Helper()
|
|
gotJSON := &bytes.Buffer{}
|
|
wantJSON := &bytes.Buffer{}
|
|
|
|
if err := json.Indent(gotJSON, got, "", " "); err != nil {
|
|
t.Errorf("got invalid JSON: %v", err)
|
|
}
|
|
if err := json.Indent(wantJSON, want, "", " "); err != nil {
|
|
t.Errorf("want invalid JSON: %v", err)
|
|
}
|
|
g := strings.TrimSpace(gotJSON.String())
|
|
w := strings.TrimSpace(wantJSON.String())
|
|
if g != w {
|
|
t.Errorf("wanted %q, got %q", w, g)
|
|
}
|
|
}
|
|
|
|
func TestRefreshCreds(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config api.ExecConfig
|
|
output string
|
|
interactive bool
|
|
response *clientauthentication.Response
|
|
wantInput string
|
|
wantCreds credentials
|
|
wantExpiry time.Time
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "basic-request",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {}
|
|
}`,
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"token": "foo-bar"
|
|
}
|
|
}`,
|
|
wantCreds: credentials{token: "foo-bar"},
|
|
},
|
|
{
|
|
name: "interactive",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
interactive: true,
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {
|
|
"interactive": true
|
|
}
|
|
}`,
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"token": "foo-bar"
|
|
}
|
|
}`,
|
|
wantCreds: credentials{token: "foo-bar"},
|
|
},
|
|
{
|
|
name: "response",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
response: &clientauthentication.Response{
|
|
Header: map[string][]string{
|
|
"WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`},
|
|
},
|
|
Code: 401,
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {
|
|
"response": {
|
|
"header": {
|
|
"WWW-Authenticate": [
|
|
"Basic realm=\"Access to the staging site\", charset=\"UTF-8\""
|
|
]
|
|
},
|
|
"code": 401
|
|
}
|
|
}
|
|
}`,
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"token": "foo-bar"
|
|
}
|
|
}`,
|
|
wantCreds: credentials{token: "foo-bar"},
|
|
},
|
|
{
|
|
name: "expiry",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {}
|
|
}`,
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"token": "foo-bar",
|
|
"expirationTimestamp": "2006-01-02T15:04:05Z"
|
|
}
|
|
}`,
|
|
wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC),
|
|
wantCreds: credentials{token: "foo-bar"},
|
|
},
|
|
{
|
|
name: "no-group-version",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {}
|
|
}`,
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"status": {
|
|
"token": "foo-bar"
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "no-status",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {}
|
|
}`,
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1"
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "no-creds",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {}
|
|
}`,
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"status": {}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "TLS credentials",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {}
|
|
}`,
|
|
output: fmt.Sprintf(`{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"clientKeyData": %q,
|
|
"clientCertificateData": %q
|
|
}
|
|
}`, keyData, certData),
|
|
wantCreds: credentials{cert: validCert},
|
|
},
|
|
{
|
|
name: "bad TLS credentials",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {}
|
|
}`,
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"clientKeyData": "foo",
|
|
"clientCertificateData": "bar"
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "cert but no key",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
},
|
|
wantInput: `{
|
|
"kind":"ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
|
"spec": {}
|
|
}`,
|
|
output: fmt.Sprintf(`{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"clientCertificateData": %q
|
|
}
|
|
}`, certData),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "beta-basic-request",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
|
"status": {
|
|
"token": "foo-bar"
|
|
}
|
|
}`,
|
|
wantCreds: credentials{token: "foo-bar"},
|
|
},
|
|
{
|
|
name: "beta-expiry",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
|
"status": {
|
|
"token": "foo-bar",
|
|
"expirationTimestamp": "2006-01-02T15:04:05Z"
|
|
}
|
|
}`,
|
|
wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC),
|
|
wantCreds: credentials{token: "foo-bar"},
|
|
},
|
|
{
|
|
name: "beta-no-group-version",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"status": {
|
|
"token": "foo-bar"
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "beta-no-status",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1beta1"
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "beta-no-token",
|
|
config: api.ExecConfig{
|
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
},
|
|
output: `{
|
|
"kind": "ExecCredential",
|
|
"apiVersion":"client.authentication.k8s.io/v1beta1",
|
|
"status": {}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
c := test.config
|
|
|
|
c.Command = "./testdata/test-plugin.sh"
|
|
c.Env = append(c.Env, api.ExecEnvVar{
|
|
Name: "TEST_OUTPUT",
|
|
Value: test.output,
|
|
})
|
|
|
|
a, err := newAuthenticator(newCache(), &c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stderr := &bytes.Buffer{}
|
|
a.stderr = stderr
|
|
a.interactive = test.interactive
|
|
a.environ = func() []string { return nil }
|
|
|
|
if err := a.refreshCredsLocked(test.response); err != nil {
|
|
if !test.wantErr {
|
|
t.Errorf("get token %v", err)
|
|
}
|
|
return
|
|
}
|
|
if test.wantErr {
|
|
t.Fatal("expected error getting token")
|
|
}
|
|
|
|
if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) {
|
|
t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds)
|
|
}
|
|
|
|
if !a.exp.Equal(test.wantExpiry) {
|
|
t.Errorf("expected expiry %v got %v", test.wantExpiry, a.exp)
|
|
}
|
|
|
|
if test.wantInput == "" {
|
|
if got := strings.TrimSpace(stderr.String()); got != "" {
|
|
t.Errorf("expected no input parameters, got %q", got)
|
|
}
|
|
return
|
|
}
|
|
|
|
compJSON(t, stderr.Bytes(), []byte(test.wantInput))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRoundTripper(t *testing.T) {
|
|
wantToken := ""
|
|
|
|
n := time.Now()
|
|
now := func() time.Time { return n }
|
|
|
|
env := []string{""}
|
|
environ := func() []string {
|
|
s := make([]string, len(env))
|
|
copy(s, env)
|
|
return s
|
|
}
|
|
|
|
setOutput := func(s string) {
|
|
env[0] = "TEST_OUTPUT=" + s
|
|
}
|
|
|
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
|
gotToken := ""
|
|
parts := strings.Split(r.Header.Get("Authorization"), " ")
|
|
if len(parts) > 1 && strings.EqualFold(parts[0], "bearer") {
|
|
gotToken = parts[1]
|
|
}
|
|
|
|
if wantToken != gotToken {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
fmt.Fprintln(w, "ok")
|
|
}
|
|
server := httptest.NewServer(http.HandlerFunc(handler))
|
|
|
|
c := api.ExecConfig{
|
|
Command: "./testdata/test-plugin.sh",
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
}
|
|
a, err := newAuthenticator(newCache(), &c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a.environ = environ
|
|
a.now = now
|
|
a.stderr = ioutil.Discard
|
|
|
|
tc := &transport.Config{}
|
|
if err := a.UpdateTransportConfig(tc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client := http.Client{
|
|
Transport: tc.WrapTransport(http.DefaultTransport),
|
|
}
|
|
|
|
get := func(t *testing.T, statusCode int) {
|
|
t.Helper()
|
|
resp, err := client.Get(server.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != statusCode {
|
|
t.Errorf("wanted status %d got %d", statusCode, resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
setOutput(`{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"token": "token1"
|
|
}
|
|
}`)
|
|
wantToken = "token1"
|
|
get(t, http.StatusOK)
|
|
|
|
setOutput(`{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"token": "token2"
|
|
}
|
|
}`)
|
|
// Previous token should be cached
|
|
get(t, http.StatusOK)
|
|
|
|
wantToken = "token2"
|
|
// Token is still cached, hits unauthorized but causes token to rotate.
|
|
get(t, http.StatusUnauthorized)
|
|
// Follow up request uses the rotated token.
|
|
get(t, http.StatusOK)
|
|
|
|
setOutput(`{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"token": "token3",
|
|
"expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `"
|
|
}
|
|
}`)
|
|
wantToken = "token3"
|
|
// Token is still cached, hit's unauthorized but causes rotation to token with an expiry.
|
|
get(t, http.StatusUnauthorized)
|
|
get(t, http.StatusOK)
|
|
|
|
// Move time forward 2 hours, "token3" is now expired.
|
|
n = n.Add(time.Hour * 2)
|
|
setOutput(`{
|
|
"kind": "ExecCredential",
|
|
"apiVersion": "client.authentication.k8s.io/v1alpha1",
|
|
"status": {
|
|
"token": "token4",
|
|
"expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `"
|
|
}
|
|
}`)
|
|
wantToken = "token4"
|
|
// Old token is expired, should refresh automatically without hitting a 401.
|
|
get(t, http.StatusOK)
|
|
}
|
|
|
|
func TestTLSCredentials(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
certPool := x509.NewCertPool()
|
|
cert, key := genClientCert(t)
|
|
if !certPool.AppendCertsFromPEM(cert) {
|
|
t.Fatal("failed to add client cert to CertPool")
|
|
}
|
|
|
|
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintln(w, "ok")
|
|
}))
|
|
server.TLS = &tls.Config{
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
ClientCAs: certPool,
|
|
}
|
|
server.StartTLS()
|
|
defer server.Close()
|
|
|
|
a, err := newAuthenticator(newCache(), &api.ExecConfig{
|
|
Command: "./testdata/test-plugin.sh",
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var output *clientauthentication.ExecCredential
|
|
a.environ = func() []string {
|
|
data, err := runtime.Encode(codecs.LegacyCodec(a.group), output)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return []string{"TEST_OUTPUT=" + string(data)}
|
|
}
|
|
a.now = func() time.Time { return now }
|
|
a.stderr = ioutil.Discard
|
|
|
|
// We're not interested in server's cert, this test is about client cert.
|
|
tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true}}
|
|
if err := a.UpdateTransportConfig(tc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
get := func(t *testing.T, desc string, wantErr bool) {
|
|
t.Run(desc, func(t *testing.T) {
|
|
tlsCfg, err := transport.TLSConfigFor(tc)
|
|
if err != nil {
|
|
t.Fatal("TLSConfigFor:", err)
|
|
}
|
|
client := http.Client{
|
|
Transport: &http.Transport{TLSClientConfig: tlsCfg},
|
|
}
|
|
resp, err := client.Get(server.URL)
|
|
switch {
|
|
case err != nil && !wantErr:
|
|
t.Errorf("got client.Get error: %q, want nil", err)
|
|
case err == nil && wantErr:
|
|
t.Error("got nil client.Get error, want non-nil")
|
|
}
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
}
|
|
})
|
|
}
|
|
|
|
output = &clientauthentication.ExecCredential{
|
|
Status: &clientauthentication.ExecCredentialStatus{
|
|
ClientCertificateData: string(cert),
|
|
ClientKeyData: string(key),
|
|
ExpirationTimestamp: &v1.Time{now.Add(time.Hour)},
|
|
},
|
|
}
|
|
get(t, "valid TLS cert", false)
|
|
|
|
// Advance time to force re-exec.
|
|
nCert, nKey := genClientCert(t)
|
|
now = now.Add(time.Hour * 2)
|
|
output = &clientauthentication.ExecCredential{
|
|
Status: &clientauthentication.ExecCredentialStatus{
|
|
ClientCertificateData: string(nCert),
|
|
ClientKeyData: string(nKey),
|
|
ExpirationTimestamp: &v1.Time{now.Add(time.Hour)},
|
|
},
|
|
}
|
|
get(t, "untrusted TLS cert", true)
|
|
|
|
now = now.Add(time.Hour * 2)
|
|
output = &clientauthentication.ExecCredential{
|
|
Status: &clientauthentication.ExecCredentialStatus{
|
|
ClientCertificateData: string(cert),
|
|
ClientKeyData: string(key),
|
|
ExpirationTimestamp: &v1.Time{now.Add(time.Hour)},
|
|
},
|
|
}
|
|
get(t, "valid TLS cert again", false)
|
|
}
|
|
|
|
func TestConcurrentUpdateTransportConfig(t *testing.T) {
|
|
n := time.Now()
|
|
now := func() time.Time { return n }
|
|
|
|
env := []string{""}
|
|
environ := func() []string {
|
|
s := make([]string, len(env))
|
|
copy(s, env)
|
|
return s
|
|
}
|
|
|
|
c := api.ExecConfig{
|
|
Command: "./testdata/test-plugin.sh",
|
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
|
}
|
|
a, err := newAuthenticator(newCache(), &c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a.environ = environ
|
|
a.now = now
|
|
a.stderr = ioutil.Discard
|
|
|
|
stopCh := make(chan struct{})
|
|
defer close(stopCh)
|
|
|
|
numConcurrent := 2
|
|
|
|
for i := 0; i < numConcurrent; i++ {
|
|
go func() {
|
|
for {
|
|
tc := &transport.Config{}
|
|
a.UpdateTransportConfig(tc)
|
|
|
|
select {
|
|
case <-stopCh:
|
|
return
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
|
|
// genClientCert generates an x509 certificate for testing. Certificate and key
|
|
// are returned in PEM encoding.
|
|
func genClientCert(t *testing.T) ([]byte, []byte) {
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
keyRaw, err := x509.MarshalECPrivateKey(key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cert := &x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{Organization: []string{"Acme Co"}},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(24 * time.Hour),
|
|
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}),
|
|
pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw})
|
|
}
|