exec credential provider: wire in cluster info

Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
Monis Khan 2020-05-06 01:01:09 -04:00 committed by Andrew Keesler
parent 5f2ebe4bbc
commit f97422c8bd
No known key found for this signature in database
GPG Key ID: 27CE0444346F9413
10 changed files with 431 additions and 60 deletions

View File

@ -246,7 +246,7 @@ func restConfigFromKubeconfig(configAuthInfo *clientcmdapi.AuthInfo) (*rest.Conf
config.Password = configAuthInfo.Password config.Password = configAuthInfo.Password
} }
if configAuthInfo.Exec != nil { if configAuthInfo.Exec != nil {
config.ExecProvider = configAuthInfo.Exec.DeepCopy() config.Exec.ExecProvider = configAuthInfo.Exec.DeepCopy()
} }
if configAuthInfo.AuthProvider != nil { if configAuthInfo.AuthProvider != nil {
return nil, fmt.Errorf("auth provider not supported") return nil, fmt.Errorf("auth provider not supported")

View File

@ -18,6 +18,7 @@ package clientauthentication
import ( import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
) )
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -49,6 +50,10 @@ type ExecCredentialSpec struct {
// interactive prompt. // interactive prompt.
// +optional // +optional
Interactive bool Interactive bool
// Cluster contains information to allow an exec plugin to communicate
// with the kubernetes cluster being authenticated to.
Cluster Cluster
} }
// ExecCredentialStatus holds credentials for the transport to use. // ExecCredentialStatus holds credentials for the transport to use.
@ -75,3 +80,42 @@ type Response struct {
// Code is the HTTP status code returned by the server. // Code is the HTTP status code returned by the server.
Code int32 Code int32
} }
// Cluster contains information to allow an exec plugin to communicate
// with the kubernetes cluster being authenticated to.
type Cluster struct {
// Server is the address of the kubernetes cluster (https://hostname:port).
Server string
// ServerName is passed to the server for SNI and is used in the client to check server
// certificates against. If ServerName is empty, the hostname used to contact the
// server is used.
// +optional
ServerName string
// CAData contains PEM-encoded certificate authority certificates.
// If empty, system roots should be used.
// +listType=atomic
// +optional
CAData []byte
// Config holds additional config data that is specific to the exec
// plugin with regards to the cluster being authenticated to.
//
// This data is sourced from the clientcmd Cluster object's extensions[exec] field:
//
// clusters:
// - name: my-cluster
// cluster:
// ...
// extensions:
// - name: exec # reserved extension name for per cluster exec config
// extension:
// audience: 06e3fbd18de8 # arbitrary config
//
// In some environments, the user config may be exactly the same across many clusters
// (i.e. call this exec plugin) minus some details that are specific to each cluster
// such as the audience. This field allows the per cluster config to be directly
// specified with the cluster info. Using this field to store secret data is not
// recommended as one of the prime benefits of exec plugins is that no secrets need
// to be stored directly in the kubeconfig.
// +optional
Config runtime.Object
}

View File

@ -18,17 +18,17 @@ package v1beta1
import ( import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
) )
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// ExecCredentials is used by exec-based plugins to communicate credentials to // ExecCredential is used by exec-based plugins to communicate credentials to
// HTTP transports. // HTTP transports.
type ExecCredential struct { type ExecCredential struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
// Spec holds information passed to the plugin by the transport. This contains // Spec holds information passed to the plugin by the transport.
// request and runtime specific information, such as if the session is interactive.
Spec ExecCredentialSpec `json:"spec,omitempty"` Spec ExecCredentialSpec `json:"spec,omitempty"`
// Status is filled in by the plugin and holds the credentials that the transport // Status is filled in by the plugin and holds the credentials that the transport
@ -37,9 +37,13 @@ type ExecCredential struct {
Status *ExecCredentialStatus `json:"status,omitempty"` Status *ExecCredentialStatus `json:"status,omitempty"`
} }
// ExecCredenitalSpec holds request and runtime specific information provided by // ExecCredentialSpec holds request and runtime specific information provided by
// the transport. // the transport.
type ExecCredentialSpec struct{} type ExecCredentialSpec struct {
// Cluster contains information to allow an exec plugin to communicate
// with the kubernetes cluster being authenticated to.
Cluster Cluster `json:"cluster"`
}
// ExecCredentialStatus holds credentials for the transport to use. // ExecCredentialStatus holds credentials for the transport to use.
// //
@ -57,3 +61,42 @@ type ExecCredentialStatus struct {
// PEM-encoded private key for the above certificate. // PEM-encoded private key for the above certificate.
ClientKeyData string `json:"clientKeyData,omitempty"` ClientKeyData string `json:"clientKeyData,omitempty"`
} }
// Cluster contains information to allow an exec plugin to communicate
// with the kubernetes cluster being authenticated to.
type Cluster struct {
// Server is the address of the kubernetes cluster (https://hostname:port).
Server string `json:"server"`
// ServerName is passed to the server for SNI and is used in the client to check server
// certificates against. If ServerName is empty, the hostname used to contact the
// server is used.
// +optional
ServerName string `json:"serverName,omitempty"`
// CAData contains PEM-encoded certificate authority certificates.
// If empty, system roots should be used.
// +listType=atomic
// +optional
CAData []byte `json:"caData,omitempty"`
// Config holds additional config data that is specific to the exec
// plugin with regards to the cluster being authenticated to.
//
// This data is sourced from the clientcmd Cluster object's extensions[exec] field:
//
// clusters:
// - name: my-cluster
// cluster:
// ...
// extensions:
// - name: exec # reserved extension name for per cluster exec config
// extension:
// audience: 06e3fbd18de8 # arbitrary config
//
// In some environments, the user config may be exactly the same across many clusters
// (i.e. call this exec plugin) minus some details that are specific to each cluster
// such as the audience. This field allows the per cluster config to be directly
// specified with the cluster info. Using this field to store secret data is not
// recommended as one of the prime benefits of exec plugins is that no secrets need
// to be stored directly in the kubeconfig.
// +optional
Config runtime.RawExtension `json:"config,omitempty"`
}

View File

@ -87,8 +87,15 @@ func newCache() *cache {
var spewConfig = &spew.ConfigState{DisableMethods: true, Indent: " "} var spewConfig = &spew.ConfigState{DisableMethods: true, Indent: " "}
func cacheKey(c *api.ExecConfig) string { func cacheKey(conf *api.ExecConfig, cluster clientauthentication.Cluster) string {
return spewConfig.Sprint(c) key := struct {
conf *api.ExecConfig
cluster clientauthentication.Cluster
}{
conf: conf,
cluster: cluster,
}
return spewConfig.Sprint(key)
} }
type cache struct { type cache struct {
@ -155,12 +162,12 @@ func (s *sometimes) Do(f func()) {
} }
// GetAuthenticator returns an exec-based plugin for providing client credentials. // GetAuthenticator returns an exec-based plugin for providing client credentials.
func GetAuthenticator(config *api.ExecConfig) (*Authenticator, error) { func GetAuthenticator(config *api.ExecConfig, cluster clientauthentication.Cluster) (*Authenticator, error) {
return newAuthenticator(globalCache, config) return newAuthenticator(globalCache, config, cluster)
} }
func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error) { func newAuthenticator(c *cache, config *api.ExecConfig, cluster clientauthentication.Cluster) (*Authenticator, error) {
key := cacheKey(config) key := cacheKey(config, cluster)
if a, ok := c.get(key); ok { if a, ok := c.get(key); ok {
return a, nil return a, nil
} }
@ -171,9 +178,10 @@ func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error)
} }
a := &Authenticator{ a := &Authenticator{
cmd: config.Command, cmd: config.Command,
args: config.Args, args: config.Args,
group: gv, group: gv,
cluster: cluster,
installHint: config.InstallHint, installHint: config.InstallHint,
sometimes: &sometimes{ sometimes: &sometimes{
@ -200,10 +208,11 @@ func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error)
// The plugin input and output are defined by the API group client.authentication.k8s.io. // The plugin input and output are defined by the API group client.authentication.k8s.io.
type Authenticator struct { type Authenticator struct {
// Set by the config // Set by the config
cmd string cmd string
args []string args []string
group schema.GroupVersion group schema.GroupVersion
env []string env []string
cluster clientauthentication.Cluster
// Used to avoid log spew by rate limiting install hint printing. We didn't do // Used to avoid log spew by rate limiting install hint printing. We didn't do
// this by interval based rate limiting alone since that way may have prevented // this by interval based rate limiting alone since that way may have prevented
@ -365,21 +374,16 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
Spec: clientauthentication.ExecCredentialSpec{ Spec: clientauthentication.ExecCredentialSpec{
Response: r, Response: r,
Interactive: a.interactive, Interactive: a.interactive,
Cluster: a.cluster,
}, },
} }
env := append(a.environ(), a.env...) env := append(a.environ(), a.env...)
if a.group == v1alpha1.SchemeGroupVersion { data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
// Input spec disabled for beta due to lack of use. Possibly re-enable this later if if err != nil {
// someone wants it back. return fmt.Errorf("encode ExecCredentials: %v", err)
//
// See: https://github.com/kubernetes/kubernetes/issues/61796
data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
if err != nil {
return fmt.Errorf("encode ExecCredentials: %v", err)
}
env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data))
} }
env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data))
stdout := &bytes.Buffer{} stdout := &bytes.Buffer{}
cmd := exec.Command(a.cmd, a.args...) cmd := exec.Command(a.cmd, a.args...)

View File

@ -117,6 +117,21 @@ func TestCacheKey(t *testing.T) {
}, },
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
} }
c1c := clientauthentication.Cluster{
Server: "foo",
ServerName: "bar",
CAData: []byte("baz"),
Config: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "",
Kind: "",
},
Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
ContentEncoding: "",
ContentType: "application/json",
},
}
c2 := &api.ExecConfig{ c2 := &api.ExecConfig{
Command: "foo-bar", Command: "foo-bar",
Args: []string{"1", "2"}, Args: []string{"1", "2"},
@ -127,6 +142,21 @@ func TestCacheKey(t *testing.T) {
}, },
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
} }
c2c := clientauthentication.Cluster{
Server: "foo",
ServerName: "bar",
CAData: []byte("baz"),
Config: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "",
Kind: "",
},
Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
ContentEncoding: "",
ContentType: "application/json",
},
}
c3 := &api.ExecConfig{ c3 := &api.ExecConfig{
Command: "foo-bar", Command: "foo-bar",
Args: []string{"1", "2"}, Args: []string{"1", "2"},
@ -136,9 +166,49 @@ func TestCacheKey(t *testing.T) {
}, },
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
} }
key1 := cacheKey(c1) c3c := clientauthentication.Cluster{
key2 := cacheKey(c2) Server: "foo",
key3 := cacheKey(c3) ServerName: "bar",
CAData: []byte("baz"),
Config: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "",
Kind: "",
},
Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
ContentEncoding: "",
ContentType: "application/json",
},
}
c4 := &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",
}
c4c := clientauthentication.Cluster{
Server: "foo",
ServerName: "bar",
CAData: []byte("baz"),
Config: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "",
Kind: "",
},
Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`),
ContentEncoding: "",
ContentType: "application/json",
},
}
key1 := cacheKey(c1, c1c)
key2 := cacheKey(c2, c2c)
key3 := cacheKey(c3, c3c)
key4 := cacheKey(c4, c4c)
if key1 != key2 { if key1 != key2 {
t.Error("key1 and key2 didn't match") t.Error("key1 and key2 didn't match")
} }
@ -148,6 +218,9 @@ func TestCacheKey(t *testing.T) {
if key2 == key3 { if key2 == key3 {
t.Error("key2 and key3 matched") t.Error("key2 and key3 matched")
} }
if key3 == key4 {
t.Error("key3 and key4 matched")
}
} }
func compJSON(t *testing.T, got, want []byte) { func compJSON(t *testing.T, got, want []byte) {
@ -173,6 +246,7 @@ func TestRefreshCreds(t *testing.T) {
name string name string
config api.ExecConfig config api.ExecConfig
exitCode int exitCode int
cluster clientauthentication.Cluster
output string output string
interactive bool interactive bool
response *clientauthentication.Response response *clientauthentication.Response
@ -393,6 +467,16 @@ func TestRefreshCreds(t *testing.T) {
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
}, },
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"cluster": {
"server": "",
"config": null
}
}
}`,
output: `{ output: `{
"kind": "ExecCredential", "kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1", "apiVersion": "client.authentication.k8s.io/v1beta1",
@ -407,6 +491,16 @@ func TestRefreshCreds(t *testing.T) {
config: api.ExecConfig{ config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1", APIVersion: "client.authentication.k8s.io/v1beta1",
}, },
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {
"cluster": {
"server": "",
"config": null
}
}
}`,
output: `{ output: `{
"kind": "ExecCredential", "kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1", "apiVersion": "client.authentication.k8s.io/v1beta1",
@ -473,6 +567,106 @@ func TestRefreshCreds(t *testing.T) {
wantErr: true, wantErr: true,
wantErrSubstr: "73", wantErrSubstr: "73",
}, },
{
name: "alpha-with-cluster-is-ignored",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
cluster: clientauthentication.Cluster{
Server: "foo",
ServerName: "bar",
CAData: []byte("baz"),
Config: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "",
Kind: "",
},
Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`),
ContentEncoding: "",
ContentType: "application/json",
},
},
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: "beta-with-cluster-is-serialized",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
},
cluster: clientauthentication.Cluster{
Server: "foo",
ServerName: "bar",
CAData: []byte("baz"),
Config: &runtime.Unknown{
TypeMeta: runtime.TypeMeta{
APIVersion: "",
Kind: "",
},
Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`),
ContentEncoding: "",
ContentType: "application/json",
},
},
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/v1beta1",
"spec": {
"cluster": {
"server": "foo",
"serverName": "bar",
"caData": "YmF6",
"config": {
"apiVersion": "group/v1",
"kind": "PluginConfig",
"spec": {
"audience": "snorlax"
}
}
}
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"status": {
"token": "foo-bar"
}
}`,
wantCreds: credentials{token: "foo-bar"},
},
} }
for _, test := range tests { for _, test := range tests {
@ -491,7 +685,7 @@ func TestRefreshCreds(t *testing.T) {
}) })
} }
a, err := newAuthenticator(newCache(), &c) a, err := newAuthenticator(newCache(), &c, test.cluster)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -569,7 +763,7 @@ func TestRoundTripper(t *testing.T) {
Command: "./testdata/test-plugin.sh", Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
} }
a, err := newAuthenticator(newCache(), &c) a, err := newAuthenticator(newCache(), &c, clientauthentication.Cluster{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -694,7 +888,7 @@ func TestTLSCredentials(t *testing.T) {
a, err := newAuthenticator(newCache(), &api.ExecConfig{ a, err := newAuthenticator(newCache(), &api.ExecConfig{
Command: "./testdata/test-plugin.sh", Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
}) }, clientauthentication.Cluster{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -784,7 +978,7 @@ func TestConcurrentUpdateTransportConfig(t *testing.T) {
Command: "./testdata/test-plugin.sh", Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1", APIVersion: "client.authentication.k8s.io/v1alpha1",
} }
a, err := newAuthenticator(newCache(), &c) a, err := newAuthenticator(newCache(), &c, clientauthentication.Cluster{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -87,7 +87,7 @@ type Config struct {
AuthConfigPersister AuthProviderConfigPersister AuthConfigPersister AuthProviderConfigPersister
// Exec-based authentication provider. // Exec-based authentication provider.
ExecProvider *clientcmdapi.ExecConfig Exec Exec
// TLSClientConfig contains settings to enable transport layer security // TLSClientConfig contains settings to enable transport layer security
TLSClientConfig TLSClientConfig
@ -160,6 +160,15 @@ func (sanitizedAuthConfigPersister) String() string {
return "rest.AuthProviderConfigPersister(--- REDACTED ---)" return "rest.AuthProviderConfigPersister(--- REDACTED ---)"
} }
type sanitizedObject struct{ runtime.Object }
func (sanitizedObject) GoString() string {
return "runtime.Object(--- REDACTED ---)"
}
func (sanitizedObject) String() string {
return "runtime.Object(--- REDACTED ---)"
}
// GoString implements fmt.GoStringer and sanitizes sensitive fields of Config // GoString implements fmt.GoStringer and sanitizes sensitive fields of Config
// to prevent accidental leaking via logs. // to prevent accidental leaking via logs.
func (c *Config) GoString() string { func (c *Config) GoString() string {
@ -183,10 +192,40 @@ func (c *Config) String() string {
if cc.AuthConfigPersister != nil { if cc.AuthConfigPersister != nil {
cc.AuthConfigPersister = sanitizedAuthConfigPersister{cc.AuthConfigPersister} cc.AuthConfigPersister = sanitizedAuthConfigPersister{cc.AuthConfigPersister}
} }
if cc.Exec.Config != nil {
cc.Exec.Config = sanitizedObject{Object: cc.Exec.Config}
}
return fmt.Sprintf("%#v", cc) return fmt.Sprintf("%#v", cc)
} }
// Exec plugin authentication provider.
type Exec struct {
// ExecProvider provides the config needed to execute the exec plugin.
ExecProvider *clientcmdapi.ExecConfig
// Config holds additional config data that is specific to the exec
// plugin with regards to the cluster being authenticated to.
//
// This data is sourced from the clientcmd Cluster object's extensions[exec] field:
//
// clusters:
// - name: my-cluster
// cluster:
// ...
// extensions:
// - name: exec # reserved extension name for per cluster exec config
// extension:
// audience: 06e3fbd18de8 # arbitrary config
//
// In some environments, the user config may be exactly the same across many clusters
// (i.e. call this exec plugin) minus some details that are specific to each cluster
// such as the audience. This field allows the per cluster config to be directly
// specified with the cluster info. Using this field to store secret data is not
// recommended as one of the prime benefits of exec plugins is that no secrets need
// to be stored directly in the kubeconfig.
Config runtime.Object
}
// ImpersonationConfig has all the available impersonation options // ImpersonationConfig has all the available impersonation options
type ImpersonationConfig struct { type ImpersonationConfig struct {
// UserName is the username to impersonate on each request. // UserName is the username to impersonate on each request.
@ -603,7 +642,10 @@ func CopyConfig(config *Config) *Config {
}, },
AuthProvider: config.AuthProvider, AuthProvider: config.AuthProvider,
AuthConfigPersister: config.AuthConfigPersister, AuthConfigPersister: config.AuthConfigPersister,
ExecProvider: config.ExecProvider, Exec: Exec{
ExecProvider: config.Exec.ExecProvider,
Config: config.Exec.Config,
},
TLSClientConfig: TLSClientConfig{ TLSClientConfig: TLSClientConfig{
Insecure: config.TLSClientConfig.Insecure, Insecure: config.TLSClientConfig.Insecure,
ServerName: config.TLSClientConfig.ServerName, ServerName: config.TLSClientConfig.ServerName,

View File

@ -337,6 +337,11 @@ func TestAnonymousConfig(t *testing.T) {
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) { func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
*r = fakeProxyFunc *r = fakeProxyFunc
}, },
func(r *runtime.Object, f fuzz.Continue) {
unknown := &runtime.Unknown{}
f.Fuzz(unknown)
*r = unknown
},
) )
for i := 0; i < 20; i++ { for i := 0; i < 20; i++ {
original := &Config{} original := &Config{}
@ -353,7 +358,8 @@ func TestAnonymousConfig(t *testing.T) {
expected.Password = "" expected.Password = ""
expected.AuthProvider = nil expected.AuthProvider = nil
expected.AuthConfigPersister = nil expected.AuthConfigPersister = nil
expected.ExecProvider = nil expected.Exec.ExecProvider = nil
expected.Exec.Config = nil
expected.TLSClientConfig.CertData = nil expected.TLSClientConfig.CertData = nil
expected.TLSClientConfig.CertFile = "" expected.TLSClientConfig.CertFile = ""
expected.TLSClientConfig.KeyData = nil expected.TLSClientConfig.KeyData = nil
@ -428,6 +434,11 @@ func TestCopyConfig(t *testing.T) {
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) { func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
*r = fakeProxyFunc *r = fakeProxyFunc
}, },
func(r *runtime.Object, f fuzz.Continue) {
unknown := &runtime.Unknown{}
f.Fuzz(unknown)
*r = unknown
},
) )
for i := 0; i < 20; i++ { for i := 0; i < 20; i++ {
original := &Config{} original := &Config{}
@ -524,9 +535,12 @@ func TestConfigStringer(t *testing.T) {
AuthProvider: &clientcmdapi.AuthProviderConfig{ AuthProvider: &clientcmdapi.AuthProviderConfig{
Config: map[string]string{"secret": "s3cr3t"}, Config: map[string]string{"secret": "s3cr3t"},
}, },
ExecProvider: &clientcmdapi.ExecConfig{ Exec: Exec{
Args: []string{"secret"}, ExecProvider: &clientcmdapi.ExecConfig{
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, Args: []string{"secret"},
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
},
Config: &runtime.Unknown{Raw: []byte("super secret password")},
}, },
}, },
expectContent: []string{ expectContent: []string{
@ -545,6 +559,8 @@ func TestConfigStringer(t *testing.T) {
formatBytes([]byte("fake key")), formatBytes([]byte("fake key")),
"secret", "secret",
"s3cr3t", "s3cr3t",
"super secret password",
formatBytes([]byte("super secret password")),
}, },
}, },
} }
@ -587,10 +603,13 @@ func TestConfigSprint(t *testing.T) {
Config: map[string]string{"secret": "s3cr3t"}, Config: map[string]string{"secret": "s3cr3t"},
}, },
AuthConfigPersister: fakeAuthProviderConfigPersister{}, AuthConfigPersister: fakeAuthProviderConfigPersister{},
ExecProvider: &clientcmdapi.ExecConfig{ Exec: Exec{
Command: "sudo", ExecProvider: &clientcmdapi.ExecConfig{
Args: []string{"secret"}, Command: "sudo",
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, Args: []string{"secret"},
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
},
Config: &runtime.Unknown{Raw: []byte("super secret password")},
}, },
TLSClientConfig: TLSClientConfig{ TLSClientConfig: TLSClientConfig{
CertFile: "a.crt", CertFile: "a.crt",
@ -611,7 +630,7 @@ func TestConfigSprint(t *testing.T) {
Proxy: fakeProxyFunc, Proxy: fakeProxyFunc,
} }
want := fmt.Sprintf( want := fmt.Sprintf(
`&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), WarningHandler:rest.fakeWarningHandler{}, Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`, `&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), Exec:rest.Exec{ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, Config:runtime.Object(--- REDACTED ---)}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), WarningHandler:rest.fakeWarningHandler{}, Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`,
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc, c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc,
) )

View File

@ -19,8 +19,10 @@ package rest
import ( import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"net/http" "net/http"
"k8s.io/client-go/pkg/apis/clientauthentication"
"k8s.io/client-go/plugin/pkg/client/auth/exec" "k8s.io/client-go/plugin/pkg/client/auth/exec"
"k8s.io/client-go/transport" "k8s.io/client-go/transport"
) )
@ -89,12 +91,22 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
Proxy: c.Proxy, Proxy: c.Proxy,
} }
if c.ExecProvider != nil && c.AuthProvider != nil { if c.Exec.ExecProvider != nil && c.AuthProvider != nil {
return nil, errors.New("execProvider and authProvider cannot be used in combination") return nil, errors.New("execProvider and authProvider cannot be used in combination")
} }
if c.ExecProvider != nil { if c.Exec.ExecProvider != nil {
provider, err := exec.GetAuthenticator(c.ExecProvider) caData, err := dataFromSliceOrFile(c.CAData, c.CAFile)
if err != nil {
return nil, fmt.Errorf("failed to load CA bundle for execProvider: %v", err)
}
cluster := clientauthentication.Cluster{
Server: c.Host,
ServerName: c.TLSClientConfig.ServerName,
CAData: caData,
Config: c.Exec.Config,
}
provider, err := exec.GetAuthenticator(c.Exec.ExecProvider, cluster)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -189,7 +189,7 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
authInfoName, _ := config.getAuthInfoName() authInfoName, _ := config.getAuthInfoName()
persister = PersisterForUser(config.configAccess, authInfoName) persister = PersisterForUser(config.configAccess, authInfoName)
} }
userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister) userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister, configClusterInfo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -232,7 +232,7 @@ func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo,
// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority)
// 3. if there is not enough information to identify the user, load try the ~/.kubernetes_auth file // 3. if there is not enough information to identify the user, load try the ~/.kubernetes_auth file
// 4. if there is not enough information to identify the user, prompt if possible // 4. if there is not enough information to identify the user, prompt if possible
func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister) (*restclient.Config, error) { func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister, configClusterInfo clientcmdapi.Cluster) (*restclient.Config, error) {
mergedConfig := &restclient.Config{} mergedConfig := &restclient.Config{}
// blindly overwrite existing values based on precedence // blindly overwrite existing values based on precedence
@ -269,8 +269,9 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI
mergedConfig.AuthConfigPersister = persistAuthConfig mergedConfig.AuthConfigPersister = persistAuthConfig
} }
if configAuthInfo.Exec != nil { if configAuthInfo.Exec != nil {
mergedConfig.ExecProvider = configAuthInfo.Exec mergedConfig.Exec.ExecProvider = configAuthInfo.Exec
mergedConfig.ExecProvider.InstallHint = cleanANSIEscapeCodes(mergedConfig.ExecProvider.InstallHint) mergedConfig.ExecProvider.InstallHint = cleanANSIEscapeCodes(mergedConfig.ExecProvider.InstallHint)
mergedConfig.Exec.Config = configClusterInfo.Extensions["exec"] // this key is reserved in the extensions list for exec plugin config
} }
// if there still isn't enough information to authenticate the user, try prompting // if there still isn't enough information to authenticate the user, try prompting
@ -313,7 +314,7 @@ func canIdentifyUser(config restclient.Config) bool {
(len(config.CertFile) > 0 || len(config.CertData) > 0) || (len(config.CertFile) > 0 || len(config.CertData) > 0) ||
len(config.BearerToken) > 0 || len(config.BearerToken) > 0 ||
config.AuthProvider != nil || config.AuthProvider != nil ||
config.ExecProvider != nil config.Exec.ExecProvider != nil
} }
// cleanANSIEscapeCodes takes an arbitrary string and ensures that there are no // cleanANSIEscapeCodes takes an arbitrary string and ensures that there are no

View File

@ -23,10 +23,11 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/imdario/mergo"
"k8s.io/apimachinery/pkg/runtime"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"github.com/imdario/mergo"
) )
func TestMergoSemantics(t *testing.T) { func TestMergoSemantics(t *testing.T) {
@ -834,6 +835,11 @@ apiVersion: v1
clusters: clusters:
- cluster: - cluster:
server: https://localhost:8080 server: https://localhost:8080
extensions:
- name: exec
extension:
audience: foo
other: bar
name: foo-cluster name: foo-cluster
contexts: contexts:
- context: - context:
@ -865,10 +871,16 @@ users:
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
if !reflect.DeepEqual(config.ExecProvider.Args, []string{"arg-1", "arg-2"}) { if !reflect.DeepEqual(config.Exec.ExecProvider.Args, []string{"arg-1", "arg-2"}) {
t.Errorf("Got args %v when they should be %v\n", config.ExecProvider.Args, []string{"arg-1", "arg-2"}) t.Errorf("Got args %v when they should be %v\n", config.Exec.ExecProvider.Args, []string{"arg-1", "arg-2"})
}
want := &runtime.Unknown{
Raw: []byte(`{"audience":"foo","other":"bar"}`),
ContentType: "application/json",
}
if !reflect.DeepEqual(config.Exec.Config, want) {
t.Errorf("Got config %v when it should be %v\n", config.Exec.Config, want)
} }
} }
func TestCleanANSIEscapeCodes(t *testing.T) { func TestCleanANSIEscapeCodes(t *testing.T) {