mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 12:15:52 +00:00
exec credential provider: wire in cluster info
Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
parent
5f2ebe4bbc
commit
f97422c8bd
@ -246,7 +246,7 @@ func restConfigFromKubeconfig(configAuthInfo *clientcmdapi.AuthInfo) (*rest.Conf
|
||||
config.Password = configAuthInfo.Password
|
||||
}
|
||||
if configAuthInfo.Exec != nil {
|
||||
config.ExecProvider = configAuthInfo.Exec.DeepCopy()
|
||||
config.Exec.ExecProvider = configAuthInfo.Exec.DeepCopy()
|
||||
}
|
||||
if configAuthInfo.AuthProvider != nil {
|
||||
return nil, fmt.Errorf("auth provider not supported")
|
||||
|
@ -18,6 +18,7 @@ package clientauthentication
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
@ -49,6 +50,10 @@ type ExecCredentialSpec struct {
|
||||
// interactive prompt.
|
||||
// +optional
|
||||
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.
|
||||
@ -75,3 +80,42 @@ type Response struct {
|
||||
// Code is the HTTP status code returned by the server.
|
||||
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
|
||||
}
|
||||
|
@ -18,17 +18,17 @@ package v1beta1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// +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.
|
||||
type ExecCredential struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// Spec holds information passed to the plugin by the transport. This contains
|
||||
// request and runtime specific information, such as if the session is interactive.
|
||||
// Spec holds information passed to the plugin by the transport.
|
||||
Spec ExecCredentialSpec `json:"spec,omitempty"`
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// ExecCredenitalSpec holds request and runtime specific information provided by
|
||||
// ExecCredentialSpec holds request and runtime specific information provided by
|
||||
// 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.
|
||||
//
|
||||
@ -57,3 +61,42 @@ type ExecCredentialStatus struct {
|
||||
// PEM-encoded private key for the above certificate.
|
||||
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"`
|
||||
}
|
||||
|
@ -87,8 +87,15 @@ func newCache() *cache {
|
||||
|
||||
var spewConfig = &spew.ConfigState{DisableMethods: true, Indent: " "}
|
||||
|
||||
func cacheKey(c *api.ExecConfig) string {
|
||||
return spewConfig.Sprint(c)
|
||||
func cacheKey(conf *api.ExecConfig, cluster clientauthentication.Cluster) string {
|
||||
key := struct {
|
||||
conf *api.ExecConfig
|
||||
cluster clientauthentication.Cluster
|
||||
}{
|
||||
conf: conf,
|
||||
cluster: cluster,
|
||||
}
|
||||
return spewConfig.Sprint(key)
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
@ -155,12 +162,12 @@ func (s *sometimes) Do(f func()) {
|
||||
}
|
||||
|
||||
// GetAuthenticator returns an exec-based plugin for providing client credentials.
|
||||
func GetAuthenticator(config *api.ExecConfig) (*Authenticator, error) {
|
||||
return newAuthenticator(globalCache, config)
|
||||
func GetAuthenticator(config *api.ExecConfig, cluster clientauthentication.Cluster) (*Authenticator, error) {
|
||||
return newAuthenticator(globalCache, config, cluster)
|
||||
}
|
||||
|
||||
func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error) {
|
||||
key := cacheKey(config)
|
||||
func newAuthenticator(c *cache, config *api.ExecConfig, cluster clientauthentication.Cluster) (*Authenticator, error) {
|
||||
key := cacheKey(config, cluster)
|
||||
if a, ok := c.get(key); ok {
|
||||
return a, nil
|
||||
}
|
||||
@ -171,9 +178,10 @@ func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error)
|
||||
}
|
||||
|
||||
a := &Authenticator{
|
||||
cmd: config.Command,
|
||||
args: config.Args,
|
||||
group: gv,
|
||||
cmd: config.Command,
|
||||
args: config.Args,
|
||||
group: gv,
|
||||
cluster: cluster,
|
||||
|
||||
installHint: config.InstallHint,
|
||||
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.
|
||||
type Authenticator struct {
|
||||
// Set by the config
|
||||
cmd string
|
||||
args []string
|
||||
group schema.GroupVersion
|
||||
env []string
|
||||
cmd string
|
||||
args []string
|
||||
group schema.GroupVersion
|
||||
env []string
|
||||
cluster clientauthentication.Cluster
|
||||
|
||||
// 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
|
||||
@ -365,21 +374,16 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
|
||||
Spec: clientauthentication.ExecCredentialSpec{
|
||||
Response: r,
|
||||
Interactive: a.interactive,
|
||||
Cluster: a.cluster,
|
||||
},
|
||||
}
|
||||
|
||||
env := append(a.environ(), a.env...)
|
||||
if a.group == v1alpha1.SchemeGroupVersion {
|
||||
// Input spec disabled for beta due to lack of use. Possibly re-enable this later if
|
||||
// someone wants it back.
|
||||
//
|
||||
// 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))
|
||||
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))
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
cmd := exec.Command(a.cmd, a.args...)
|
||||
|
@ -117,6 +117,21 @@ func TestCacheKey(t *testing.T) {
|
||||
},
|
||||
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{
|
||||
Command: "foo-bar",
|
||||
Args: []string{"1", "2"},
|
||||
@ -127,6 +142,21 @@ func TestCacheKey(t *testing.T) {
|
||||
},
|
||||
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{
|
||||
Command: "foo-bar",
|
||||
Args: []string{"1", "2"},
|
||||
@ -136,9 +166,49 @@ func TestCacheKey(t *testing.T) {
|
||||
},
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
}
|
||||
key1 := cacheKey(c1)
|
||||
key2 := cacheKey(c2)
|
||||
key3 := cacheKey(c3)
|
||||
c3c := 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",
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Error("key1 and key2 didn't match")
|
||||
}
|
||||
@ -148,6 +218,9 @@ func TestCacheKey(t *testing.T) {
|
||||
if key2 == key3 {
|
||||
t.Error("key2 and key3 matched")
|
||||
}
|
||||
if key3 == key4 {
|
||||
t.Error("key3 and key4 matched")
|
||||
}
|
||||
}
|
||||
|
||||
func compJSON(t *testing.T, got, want []byte) {
|
||||
@ -173,6 +246,7 @@ func TestRefreshCreds(t *testing.T) {
|
||||
name string
|
||||
config api.ExecConfig
|
||||
exitCode int
|
||||
cluster clientauthentication.Cluster
|
||||
output string
|
||||
interactive bool
|
||||
response *clientauthentication.Response
|
||||
@ -393,6 +467,16 @@ func TestRefreshCreds(t *testing.T) {
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
wantInput: `{
|
||||
"kind": "ExecCredential",
|
||||
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
||||
"spec": {
|
||||
"cluster": {
|
||||
"server": "",
|
||||
"config": null
|
||||
}
|
||||
}
|
||||
}`,
|
||||
output: `{
|
||||
"kind": "ExecCredential",
|
||||
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
||||
@ -407,6 +491,16 @@ func TestRefreshCreds(t *testing.T) {
|
||||
config: api.ExecConfig{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
wantInput: `{
|
||||
"kind": "ExecCredential",
|
||||
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
||||
"spec": {
|
||||
"cluster": {
|
||||
"server": "",
|
||||
"config": null
|
||||
}
|
||||
}
|
||||
}`,
|
||||
output: `{
|
||||
"kind": "ExecCredential",
|
||||
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
||||
@ -473,6 +567,106 @@ func TestRefreshCreds(t *testing.T) {
|
||||
wantErr: true,
|
||||
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 {
|
||||
@ -491,7 +685,7 @@ func TestRefreshCreds(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
a, err := newAuthenticator(newCache(), &c)
|
||||
a, err := newAuthenticator(newCache(), &c, test.cluster)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -569,7 +763,7 @@ func TestRoundTripper(t *testing.T) {
|
||||
Command: "./testdata/test-plugin.sh",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
}
|
||||
a, err := newAuthenticator(newCache(), &c)
|
||||
a, err := newAuthenticator(newCache(), &c, clientauthentication.Cluster{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -694,7 +888,7 @@ func TestTLSCredentials(t *testing.T) {
|
||||
a, err := newAuthenticator(newCache(), &api.ExecConfig{
|
||||
Command: "./testdata/test-plugin.sh",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
})
|
||||
}, clientauthentication.Cluster{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -784,7 +978,7 @@ func TestConcurrentUpdateTransportConfig(t *testing.T) {
|
||||
Command: "./testdata/test-plugin.sh",
|
||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||
}
|
||||
a, err := newAuthenticator(newCache(), &c)
|
||||
a, err := newAuthenticator(newCache(), &c, clientauthentication.Cluster{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ type Config struct {
|
||||
AuthConfigPersister AuthProviderConfigPersister
|
||||
|
||||
// Exec-based authentication provider.
|
||||
ExecProvider *clientcmdapi.ExecConfig
|
||||
Exec Exec
|
||||
|
||||
// TLSClientConfig contains settings to enable transport layer security
|
||||
TLSClientConfig
|
||||
@ -160,6 +160,15 @@ func (sanitizedAuthConfigPersister) String() string {
|
||||
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
|
||||
// to prevent accidental leaking via logs.
|
||||
func (c *Config) GoString() string {
|
||||
@ -183,10 +192,40 @@ func (c *Config) String() string {
|
||||
if cc.AuthConfigPersister != nil {
|
||||
cc.AuthConfigPersister = sanitizedAuthConfigPersister{cc.AuthConfigPersister}
|
||||
}
|
||||
|
||||
if cc.Exec.Config != nil {
|
||||
cc.Exec.Config = sanitizedObject{Object: cc.Exec.Config}
|
||||
}
|
||||
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
|
||||
type ImpersonationConfig struct {
|
||||
// UserName is the username to impersonate on each request.
|
||||
@ -603,7 +642,10 @@ func CopyConfig(config *Config) *Config {
|
||||
},
|
||||
AuthProvider: config.AuthProvider,
|
||||
AuthConfigPersister: config.AuthConfigPersister,
|
||||
ExecProvider: config.ExecProvider,
|
||||
Exec: Exec{
|
||||
ExecProvider: config.Exec.ExecProvider,
|
||||
Config: config.Exec.Config,
|
||||
},
|
||||
TLSClientConfig: TLSClientConfig{
|
||||
Insecure: config.TLSClientConfig.Insecure,
|
||||
ServerName: config.TLSClientConfig.ServerName,
|
||||
|
@ -337,6 +337,11 @@ func TestAnonymousConfig(t *testing.T) {
|
||||
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
|
||||
*r = fakeProxyFunc
|
||||
},
|
||||
func(r *runtime.Object, f fuzz.Continue) {
|
||||
unknown := &runtime.Unknown{}
|
||||
f.Fuzz(unknown)
|
||||
*r = unknown
|
||||
},
|
||||
)
|
||||
for i := 0; i < 20; i++ {
|
||||
original := &Config{}
|
||||
@ -353,7 +358,8 @@ func TestAnonymousConfig(t *testing.T) {
|
||||
expected.Password = ""
|
||||
expected.AuthProvider = nil
|
||||
expected.AuthConfigPersister = nil
|
||||
expected.ExecProvider = nil
|
||||
expected.Exec.ExecProvider = nil
|
||||
expected.Exec.Config = nil
|
||||
expected.TLSClientConfig.CertData = nil
|
||||
expected.TLSClientConfig.CertFile = ""
|
||||
expected.TLSClientConfig.KeyData = nil
|
||||
@ -428,6 +434,11 @@ func TestCopyConfig(t *testing.T) {
|
||||
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
|
||||
*r = fakeProxyFunc
|
||||
},
|
||||
func(r *runtime.Object, f fuzz.Continue) {
|
||||
unknown := &runtime.Unknown{}
|
||||
f.Fuzz(unknown)
|
||||
*r = unknown
|
||||
},
|
||||
)
|
||||
for i := 0; i < 20; i++ {
|
||||
original := &Config{}
|
||||
@ -524,9 +535,12 @@ func TestConfigStringer(t *testing.T) {
|
||||
AuthProvider: &clientcmdapi.AuthProviderConfig{
|
||||
Config: map[string]string{"secret": "s3cr3t"},
|
||||
},
|
||||
ExecProvider: &clientcmdapi.ExecConfig{
|
||||
Args: []string{"secret"},
|
||||
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
|
||||
Exec: Exec{
|
||||
ExecProvider: &clientcmdapi.ExecConfig{
|
||||
Args: []string{"secret"},
|
||||
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
|
||||
},
|
||||
Config: &runtime.Unknown{Raw: []byte("super secret password")},
|
||||
},
|
||||
},
|
||||
expectContent: []string{
|
||||
@ -545,6 +559,8 @@ func TestConfigStringer(t *testing.T) {
|
||||
formatBytes([]byte("fake key")),
|
||||
"secret",
|
||||
"s3cr3t",
|
||||
"super secret password",
|
||||
formatBytes([]byte("super secret password")),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -587,10 +603,13 @@ func TestConfigSprint(t *testing.T) {
|
||||
Config: map[string]string{"secret": "s3cr3t"},
|
||||
},
|
||||
AuthConfigPersister: fakeAuthProviderConfigPersister{},
|
||||
ExecProvider: &clientcmdapi.ExecConfig{
|
||||
Command: "sudo",
|
||||
Args: []string{"secret"},
|
||||
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
|
||||
Exec: Exec{
|
||||
ExecProvider: &clientcmdapi.ExecConfig{
|
||||
Command: "sudo",
|
||||
Args: []string{"secret"},
|
||||
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
|
||||
},
|
||||
Config: &runtime.Unknown{Raw: []byte("super secret password")},
|
||||
},
|
||||
TLSClientConfig: TLSClientConfig{
|
||||
CertFile: "a.crt",
|
||||
@ -611,7 +630,7 @@ func TestConfigSprint(t *testing.T) {
|
||||
Proxy: fakeProxyFunc,
|
||||
}
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -19,8 +19,10 @@ package rest
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication"
|
||||
"k8s.io/client-go/plugin/pkg/client/auth/exec"
|
||||
"k8s.io/client-go/transport"
|
||||
)
|
||||
@ -89,12 +91,22 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
|
||||
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")
|
||||
}
|
||||
|
||||
if c.ExecProvider != nil {
|
||||
provider, err := exec.GetAuthenticator(c.ExecProvider)
|
||||
if c.Exec.ExecProvider != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
|
||||
authInfoName, _ := config.getAuthInfoName()
|
||||
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 {
|
||||
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)
|
||||
// 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
|
||||
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{}
|
||||
|
||||
// blindly overwrite existing values based on precedence
|
||||
@ -269,8 +269,9 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI
|
||||
mergedConfig.AuthConfigPersister = persistAuthConfig
|
||||
}
|
||||
if configAuthInfo.Exec != nil {
|
||||
mergedConfig.ExecProvider = configAuthInfo.Exec
|
||||
mergedConfig.Exec.ExecProvider = configAuthInfo.Exec
|
||||
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
|
||||
@ -313,7 +314,7 @@ func canIdentifyUser(config restclient.Config) bool {
|
||||
(len(config.CertFile) > 0 || len(config.CertData) > 0) ||
|
||||
len(config.BearerToken) > 0 ||
|
||||
config.AuthProvider != nil ||
|
||||
config.ExecProvider != nil
|
||||
config.Exec.ExecProvider != nil
|
||||
}
|
||||
|
||||
// cleanANSIEscapeCodes takes an arbitrary string and ensures that there are no
|
||||
|
@ -23,10 +23,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
|
||||
func TestMergoSemantics(t *testing.T) {
|
||||
@ -834,6 +835,11 @@ apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://localhost:8080
|
||||
extensions:
|
||||
- name: exec
|
||||
extension:
|
||||
audience: foo
|
||||
other: bar
|
||||
name: foo-cluster
|
||||
contexts:
|
||||
- context:
|
||||
@ -865,10 +871,16 @@ users:
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !reflect.DeepEqual(config.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"})
|
||||
if !reflect.DeepEqual(config.Exec.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) {
|
||||
|
Loading…
Reference in New Issue
Block a user