diff --git a/pkg/client/restclient/config.go b/pkg/client/restclient/config.go index e391949492d..6b613de96ae 100644 --- a/pkg/client/restclient/config.go +++ b/pkg/client/restclient/config.go @@ -30,6 +30,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" + clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/crypto" "k8s.io/kubernetes/pkg/version" @@ -65,6 +66,9 @@ type Config struct { // Impersonate is the username that this RESTClient will impersonate Impersonate string + // Server requires plugin-specified authentication. + AuthProvider *clientcmdapi.AuthProviderConfig + // TLSClientConfig contains settings to enable transport layer security TLSClientConfig diff --git a/pkg/client/restclient/plugin.go b/pkg/client/restclient/plugin.go new file mode 100644 index 00000000000..80d13547b74 --- /dev/null +++ b/pkg/client/restclient/plugin.go @@ -0,0 +1,60 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 restclient + +import ( + "fmt" + "net/http" + "sync" + + "github.com/golang/glog" + + clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" +) + +type AuthProvider interface { + // WrapTransport allows the plugin to create a modified RoundTripper that + // attaches authorization headers (or other info) to requests. + WrapTransport(http.RoundTripper) http.RoundTripper +} + +type Factory func() (AuthProvider, error) + +// All registered auth provider plugins. +var pluginsLock sync.Mutex +var plugins = make(map[string]Factory) + +func RegisterAuthProviderPlugin(name string, plugin Factory) error { + pluginsLock.Lock() + defer pluginsLock.Unlock() + if _, found := plugins[name]; found { + return fmt.Errorf("Auth Provider Plugin %q was registered twice", name) + } + glog.V(4).Infof("Registered Auth Provider Plugin %q", name) + plugins[name] = plugin + return nil +} + +func GetAuthProvider(apc *clientcmdapi.AuthProviderConfig) (AuthProvider, error) { + pluginsLock.Lock() + defer pluginsLock.Unlock() + p, ok := plugins[apc.Name] + if !ok { + return nil, fmt.Errorf("No Auth Provider found for name %q", apc.Name) + } + return p() +} diff --git a/pkg/client/restclient/transport.go b/pkg/client/restclient/transport.go index f0d8229204e..8c8a4464cb8 100644 --- a/pkg/client/restclient/transport.go +++ b/pkg/client/restclient/transport.go @@ -26,14 +26,22 @@ import ( // TLSConfigFor returns a tls.Config that will provide the transport level security defined // by the provided Config. Will return nil if no transport level security is requested. func TLSConfigFor(config *Config) (*tls.Config, error) { - return transport.TLSConfigFor(config.transportConfig()) + cfg, err := config.transportConfig() + if err != nil { + return nil, err + } + return transport.TLSConfigFor(cfg) } // TransportFor returns an http.RoundTripper that will provide the authentication // or transport level security defined by the provided Config. Will return the // default http.DefaultTransport if no special case behavior is needed. func TransportFor(config *Config) (http.RoundTripper, error) { - return transport.New(config.transportConfig()) + cfg, err := config.transportConfig() + if err != nil { + return nil, err + } + return transport.New(cfg) } // HTTPWrappersForConfig wraps a round tripper with any relevant layered behavior from the @@ -41,15 +49,34 @@ func TransportFor(config *Config) (http.RoundTripper, error) { // the underlying connection (like WebSocket or HTTP2 clients). Pure HTTP clients should use // the higher level TransportFor or RESTClientFor methods. func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTripper, error) { - return transport.HTTPWrappersForConfig(config.transportConfig(), rt) + cfg, err := config.transportConfig() + if err != nil { + return nil, err + } + return transport.HTTPWrappersForConfig(cfg, rt) } // transportConfig converts a client config to an appropriate transport config. -func (c *Config) transportConfig() *transport.Config { +func (c *Config) transportConfig() (*transport.Config, error) { + wt := c.WrapTransport + if c.AuthProvider != nil { + provider, err := GetAuthProvider(c.AuthProvider) + if err != nil { + return nil, err + } + if wt != nil { + previousWT := wt + wt = func(rt http.RoundTripper) http.RoundTripper { + return provider.WrapTransport(previousWT(rt)) + } + } else { + wt = provider.WrapTransport + } + } return &transport.Config{ UserAgent: c.UserAgent, Transport: c.Transport, - WrapTransport: c.WrapTransport, + WrapTransport: wt, TLS: transport.TLSConfig{ CAFile: c.CAFile, CAData: c.CAData, @@ -63,5 +90,5 @@ func (c *Config) transportConfig() *transport.Config { Password: c.Password, BearerToken: c.BearerToken, Impersonate: c.Impersonate, - } + }, nil } diff --git a/pkg/client/restclient/transport_test.go b/pkg/client/restclient/transport_test.go new file mode 100644 index 00000000000..d94f0a99d75 --- /dev/null +++ b/pkg/client/restclient/transport_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 restclient + +import ( + "fmt" + "net/http" + "testing" + + clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" +) + +func TestTransportConfigAuthPlugins(t *testing.T) { + if err := RegisterAuthProviderPlugin("pluginA", pluginAProvider); err != nil { + t.Errorf("Unexpected error: failed to register pluginA: %v", err) + } + if err := RegisterAuthProviderPlugin("pluginB", pluginBProvider); err != nil { + t.Errorf("Unexpected error: failed to register pluginB: %v", err) + } + if err := RegisterAuthProviderPlugin("pluginFail", pluginFailProvider); err != nil { + t.Errorf("Unexpected error: failed to register pluginFail: %v", err) + } + testCases := []struct { + useWrapTransport bool + plugin string + expectErr bool + expectPluginA bool + expectPluginB bool + }{ + {false, "", false, false, false}, + {false, "pluginA", false, true, false}, + {false, "pluginB", false, false, true}, + {false, "pluginFail", true, false, false}, + {false, "pluginUnknown", true, false, false}, + } + for i, tc := range testCases { + c := Config{} + if tc.useWrapTransport { + // Specify an existing WrapTransport in the config to make sure that + // plugins play nicely. + c.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return &wrapTransport{rt} + } + } + if len(tc.plugin) != 0 { + c.AuthProvider = &clientcmdapi.AuthProviderConfig{Name: tc.plugin} + } + tConfig, err := c.transportConfig() + if err != nil { + // Unknown/bad plugins are expected to fail here. + if !tc.expectErr { + t.Errorf("%d. Did not expect errors loading Auth Plugin: %q. Got: %v", i, tc.plugin, err) + } + continue + } + var fullyWrappedTransport http.RoundTripper + fullyWrappedTransport = &emptyTransport{} + if tConfig.WrapTransport != nil { + fullyWrappedTransport = tConfig.WrapTransport(&emptyTransport{}) + } + res, err := fullyWrappedTransport.RoundTrip(&http.Request{}) + if err != nil { + t.Errorf("%d. Unexpected error in RoundTrip: %v", i, err) + continue + } + hasWrapTransport := res.Header.Get("wrapTransport") == "Y" + hasPluginA := res.Header.Get("pluginA") == "Y" + hasPluginB := res.Header.Get("pluginB") == "Y" + if hasWrapTransport != tc.useWrapTransport { + t.Errorf("%d. Expected Existing config.WrapTransport: %t; Got: %t", i, tc.useWrapTransport, hasWrapTransport) + } + if hasPluginA != tc.expectPluginA { + t.Errorf("%d. Expected Plugin A: %t; Got: %t", i, tc.expectPluginA, hasPluginA) + } + if hasPluginB != tc.expectPluginB { + t.Errorf("%d. Expected Plugin B: %t; Got: %t", i, tc.expectPluginB, hasPluginB) + } + } +} + +// emptyTransport provides an empty http.Response with an initialized header +// to allow wrapping RoundTrippers to set header values. +type emptyTransport struct{} + +func (*emptyTransport) RoundTrip(req *http.Request) (*http.Response, error) { + res := &http.Response{ + Header: make(map[string][]string), + } + return res, nil +} + +// wrapTransport sets "wrapTransport" = "Y" on the response. +type wrapTransport struct { + rt http.RoundTripper +} + +func (w *wrapTransport) RoundTrip(req *http.Request) (*http.Response, error) { + res, err := w.rt.RoundTrip(req) + if err != nil { + return nil, err + } + res.Header.Add("wrapTransport", "Y") + return res, nil +} + +// wrapTransportA sets "pluginA" = "Y" on the response. +type wrapTransportA struct { + rt http.RoundTripper +} + +func (w *wrapTransportA) RoundTrip(req *http.Request) (*http.Response, error) { + res, err := w.rt.RoundTrip(req) + if err != nil { + return nil, err + } + res.Header.Add("pluginA", "Y") + return res, nil +} + +type pluginA struct{} + +func (*pluginA) WrapTransport(rt http.RoundTripper) http.RoundTripper { + return &wrapTransportA{rt} +} + +func pluginAProvider() (AuthProvider, error) { + return &pluginA{}, nil +} + +// wrapTransportB sets "pluginB" = "Y" on the response. +type wrapTransportB struct { + rt http.RoundTripper +} + +func (w *wrapTransportB) RoundTrip(req *http.Request) (*http.Response, error) { + res, err := w.rt.RoundTrip(req) + if err != nil { + return nil, err + } + res.Header.Add("pluginB", "Y") + return res, nil +} + +type pluginB struct{} + +func (*pluginB) WrapTransport(rt http.RoundTripper) http.RoundTripper { + return &wrapTransportB{rt} +} + +func pluginBProvider() (AuthProvider, error) { + return &pluginB{}, nil +} + +// pluginFailProvider simulates a registered AuthPlugin that fails to load. +func pluginFailProvider() (AuthProvider, error) { + return nil, fmt.Errorf("Failed to load AuthProvider") +} diff --git a/pkg/client/unversioned/clientcmd/api/types.go b/pkg/client/unversioned/clientcmd/api/types.go index 04deb13539d..1b193f74627 100644 --- a/pkg/client/unversioned/clientcmd/api/types.go +++ b/pkg/client/unversioned/clientcmd/api/types.go @@ -94,6 +94,8 @@ type AuthInfo struct { Username string `json:"username,omitempty"` // Password is the password for basic authentication to the kubernetes cluster. Password string `json:"password,omitempty"` + // AuthProvider specifies a custom authentication plugin for the kubernetes cluster. + AuthProvider *AuthProviderConfig `json:"auth-provider,omitempty"` // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields Extensions map[string]runtime.Object `json:"extensions,omitempty"` } @@ -112,6 +114,11 @@ type Context struct { Extensions map[string]runtime.Object `json:"extensions,omitempty"` } +// AuthProviderConfig holds the configuration for a specified auth provider. +type AuthProviderConfig struct { + Name string `json:"name"` +} + // NewConfig is a convenience function that returns a new Config object with non-nil maps func NewConfig() *Config { return &Config{ diff --git a/pkg/client/unversioned/clientcmd/api/types_test.go b/pkg/client/unversioned/clientcmd/api/types_test.go index 398e139bdff..552b6cab849 100644 --- a/pkg/client/unversioned/clientcmd/api/types_test.go +++ b/pkg/client/unversioned/clientcmd/api/types_test.go @@ -58,14 +58,17 @@ func Example_ofOptionsConfig() { defaultConfig.AuthInfos["red-mage-via-token"] = &AuthInfo{ Token: "my-secret-token", } + defaultConfig.AuthInfos["black-mage-via-auth-provider"] = &AuthInfo{ + AuthProvider: &AuthProviderConfig{Name: "gcp"}, + } defaultConfig.Contexts["bravo-as-black-mage"] = &Context{ Cluster: "bravo", - AuthInfo: "black-mage-via-file", + AuthInfo: "black-mage-via-auth-provider", Namespace: "yankee", } defaultConfig.Contexts["alfa-as-black-mage"] = &Context{ Cluster: "alfa", - AuthInfo: "black-mage-via-file", + AuthInfo: "black-mage-via-auth-provider", Namespace: "zulu", } defaultConfig.Contexts["alfa-as-white-mage"] = &Context{ @@ -95,7 +98,7 @@ func Example_ofOptionsConfig() { // LocationOfOrigin: "" // cluster: alfa // namespace: zulu - // user: black-mage-via-file + // user: black-mage-via-auth-provider // alfa-as-white-mage: // LocationOfOrigin: "" // cluster: alfa @@ -104,11 +107,15 @@ func Example_ofOptionsConfig() { // LocationOfOrigin: "" // cluster: bravo // namespace: yankee - // user: black-mage-via-file + // user: black-mage-via-auth-provider // current-context: alfa-as-white-mage // preferences: // colors: true // users: + // black-mage-via-auth-provider: + // LocationOfOrigin: "" + // auth-provider: + // name: gcp // red-mage-via-token: // LocationOfOrigin: "" // token: my-secret-token diff --git a/pkg/client/unversioned/clientcmd/api/v1/types.go b/pkg/client/unversioned/clientcmd/api/v1/types.go index 3874fe4ed0d..54f5a80b0be 100644 --- a/pkg/client/unversioned/clientcmd/api/v1/types.go +++ b/pkg/client/unversioned/clientcmd/api/v1/types.go @@ -88,6 +88,8 @@ type AuthInfo struct { Username string `json:"username,omitempty"` // Password is the password for basic authentication to the kubernetes cluster. Password string `json:"password,omitempty"` + // AuthProvider specifies a custom authentication plugin for the kubernetes cluster. + AuthProvider *AuthProviderConfig `json:"auth-provider,omitempty"` // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields Extensions []NamedExtension `json:"extensions,omitempty"` } @@ -135,3 +137,8 @@ type NamedExtension struct { // Extension holds the extension information Extension runtime.RawExtension `json:"extension"` } + +// AuthProviderConfig holds the configuration for a specified auth provider. +type AuthProviderConfig struct { + Name string `json:"name"` +} diff --git a/pkg/client/unversioned/clientcmd/client_config.go b/pkg/client/unversioned/clientcmd/client_config.go index b84197fc68a..64d305bd875 100644 --- a/pkg/client/unversioned/clientcmd/client_config.go +++ b/pkg/client/unversioned/clientcmd/client_config.go @@ -172,6 +172,9 @@ func getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fa mergedConfig.Username = configAuthInfo.Username mergedConfig.Password = configAuthInfo.Password } + if configAuthInfo.AuthProvider != nil { + mergedConfig.AuthProvider = configAuthInfo.AuthProvider + } // if there still isn't enough information to authenticate the user, try prompting if !canIdentifyUser(*mergedConfig) && (fallbackReader != nil) { @@ -212,8 +215,8 @@ func makeServerIdentificationConfig(info clientauth.Info) restclient.Config { func canIdentifyUser(config restclient.Config) bool { return len(config.Username) > 0 || (len(config.CertFile) > 0 || len(config.CertData) > 0) || - len(config.BearerToken) > 0 - + len(config.BearerToken) > 0 || + config.AuthProvider != nil } // Namespace implements KubeConfig diff --git a/pkg/client/unversioned/helper.go b/pkg/client/unversioned/helper.go index 46232bdc12c..a81307c34c9 100644 --- a/pkg/client/unversioned/helper.go +++ b/pkg/client/unversioned/helper.go @@ -30,6 +30,8 @@ import ( "k8s.io/kubernetes/pkg/client/typed/discovery" "k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/version" + // Import solely to initialize client auth plugins. + _ "k8s.io/kubernetes/plugin/pkg/client/auth" ) const ( diff --git a/plugin/pkg/client/auth/gcp/gcp.go b/plugin/pkg/client/auth/gcp/gcp.go new file mode 100644 index 00000000000..4e4b894ceab --- /dev/null +++ b/plugin/pkg/client/auth/gcp/gcp.go @@ -0,0 +1,53 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 gcp + +import ( + "net/http" + + "github.com/golang/glog" + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + + "k8s.io/kubernetes/pkg/client/restclient" +) + +func init() { + if err := restclient.RegisterAuthProviderPlugin("gcp", newGCPAuthProvider); err != nil { + glog.Fatalf("Failed to register gcp auth plugin: %v", err) + } +} + +type gcpAuthProvider struct { + tokenSource oauth2.TokenSource +} + +func newGCPAuthProvider() (restclient.AuthProvider, error) { + ts, err := google.DefaultTokenSource(context.TODO(), "https://www.googleapis.com/auth/cloud-platform") + if err != nil { + return nil, err + } + return &gcpAuthProvider{ts}, nil +} + +func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper { + return &oauth2.Transport{ + Source: g.tokenSource, + Base: rt, + } +} diff --git a/plugin/pkg/client/auth/plugins.go b/plugin/pkg/client/auth/plugins.go new file mode 100644 index 00000000000..c93cfd1d939 --- /dev/null +++ b/plugin/pkg/client/auth/plugins.go @@ -0,0 +1,22 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 plugins + +import ( + // Initialize all known client auth plugins. + _ "k8s.io/kubernetes/plugin/pkg/client/auth/gcp" +)