diff --git a/pkg/client/restclient/config.go b/pkg/client/restclient/config.go index e739d796afb..c1d77c0e7ca 100644 --- a/pkg/client/restclient/config.go +++ b/pkg/client/restclient/config.go @@ -70,8 +70,8 @@ type Config struct { // TODO: demonstrate an OAuth2 compatible client. BearerToken string - // Impersonate is the username that this RESTClient will impersonate - Impersonate string + // Impersonate is the configuration that RESTClient will use for impersonation. + Impersonate ImpersonationConfig // Server requires plugin-specified authentication. AuthProvider *clientcmdapi.AuthProviderConfig @@ -118,6 +118,17 @@ type Config struct { // Version string } +// ImpersonationConfig has all the available impersonation options +type ImpersonationConfig struct { + // UserName is the username to impersonate on each request. + UserName string + // Groups are the groups to impersonate on each request. + Groups []string + // Extra is a free-form field which can be used to link some authentication information + // to authorization information. This field allows you to impersonate it. + Extra map[string][]string +} + // TLSClientConfig contains settings to enable transport layer security type TLSClientConfig struct { // Server requires TLS client certificate authentication diff --git a/pkg/client/restclient/config_test.go b/pkg/client/restclient/config_test.go index e1518ace24f..9264b35f46f 100644 --- a/pkg/client/restclient/config_test.go +++ b/pkg/client/restclient/config_test.go @@ -205,7 +205,7 @@ func TestAnonymousConfig(t *testing.T) { // this is the list of known security related fields, add to this list if a new field // is added to Config, update AnonymousClientConfig to preserve the field otherwise. - expected.Impersonate = "" + expected.Impersonate = ImpersonationConfig{} expected.BearerToken = "" expected.Username = "" expected.Password = "" diff --git a/pkg/client/restclient/transport.go b/pkg/client/restclient/transport.go index 5f3d671d655..48e8042ee80 100644 --- a/pkg/client/restclient/transport.go +++ b/pkg/client/restclient/transport.go @@ -89,6 +89,10 @@ func (c *Config) TransportConfig() (*transport.Config, error) { Username: c.Username, Password: c.Password, BearerToken: c.BearerToken, - Impersonate: c.Impersonate, + Impersonate: transport.ImpersonationConfig{ + UserName: c.Impersonate.UserName, + Groups: c.Impersonate.Groups, + Extra: c.Impersonate.Extra, + }, }, nil } diff --git a/pkg/client/transport/config.go b/pkg/client/transport/config.go index 6e5c68a30b2..eaad99fb2f2 100644 --- a/pkg/client/transport/config.go +++ b/pkg/client/transport/config.go @@ -34,8 +34,8 @@ type Config struct { // Bearer token for authentication BearerToken string - // Impersonate is the username that this Config will impersonate - Impersonate string + // Impersonate is the config that this Config will impersonate using + Impersonate ImpersonationConfig // Transport may be used for custom HTTP behavior. This attribute may // not be specified with the TLS client certificate options. Use @@ -50,6 +50,16 @@ type Config struct { WrapTransport func(rt http.RoundTripper) http.RoundTripper } +// ImpersonationConfig has all the available impersonation options +type ImpersonationConfig struct { + // UserName matches user.Info.GetName() + UserName string + // Groups matches user.Info.GetGroups() + Groups []string + // Extra matches user.Info.GetExtra() + Extra map[string][]string +} + // HasCA returns whether the configuration has a certificate authority or not. func (c *Config) HasCA() bool { return len(c.TLS.CAData) > 0 || len(c.TLS.CAFile) > 0 diff --git a/pkg/client/transport/round_trippers.go b/pkg/client/transport/round_trippers.go index aadf0cbf9a2..3188d062137 100644 --- a/pkg/client/transport/round_trippers.go +++ b/pkg/client/transport/round_trippers.go @@ -48,7 +48,9 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip if len(config.UserAgent) > 0 { rt = NewUserAgentRoundTripper(config.UserAgent, rt) } - if len(config.Impersonate) > 0 { + if len(config.Impersonate.UserName) > 0 || + len(config.Impersonate.Groups) > 0 || + len(config.Impersonate.Extra) > 0 { rt = NewImpersonatingRoundTripper(config.Impersonate, rt) } return rt, nil @@ -133,22 +135,53 @@ func (rt *basicAuthRoundTripper) CancelRequest(req *http.Request) { func (rt *basicAuthRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } +// These correspond to the headers used in pkg/apis/authentication. We don't want the package dependency, +// but you must not change the values. +const ( + // ImpersonateUserHeader is used to impersonate a particular user during an API server request + ImpersonateUserHeader = "Impersonate-User" + + // ImpersonateGroupHeader is used to impersonate a particular group during an API server request. + // It can be repeated multiplied times for multiple groups. + ImpersonateGroupHeader = "Impersonate-Group" + + // ImpersonateUserExtraHeaderPrefix is a prefix for a header used to impersonate an entry in the + // extra map[string][]string for user.Info. The key for the `extra` map is suffix. + // The same key can be repeated multiple times to have multiple elements in the slice under a single key. + // For instance: + // Impersonate-Extra-Foo: one + // Impersonate-Extra-Foo: two + // results in extra["Foo"] = []string{"one", "two"} + ImpersonateUserExtraHeaderPrefix = "Impersonate-Extra-" +) + type impersonatingRoundTripper struct { - impersonate string + impersonate ImpersonationConfig delegate http.RoundTripper } // NewImpersonatingRoundTripper will add an Act-As header to a request unless it has already been set. -func NewImpersonatingRoundTripper(impersonate string, delegate http.RoundTripper) http.RoundTripper { +func NewImpersonatingRoundTripper(impersonate ImpersonationConfig, delegate http.RoundTripper) http.RoundTripper { return &impersonatingRoundTripper{impersonate, delegate} } func (rt *impersonatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if len(req.Header.Get("Impersonate-User")) != 0 { + // use the user header as marker for the rest. + if len(req.Header.Get(ImpersonateUserHeader)) != 0 { return rt.delegate.RoundTrip(req) } req = cloneRequest(req) - req.Header.Set("Impersonate-User", rt.impersonate) + req.Header.Set(ImpersonateUserHeader, rt.impersonate.UserName) + + for _, group := range rt.impersonate.Groups { + req.Header.Add(ImpersonateGroupHeader, group) + } + for k, vv := range rt.impersonate.Extra { + for _, v := range vv { + req.Header.Add(ImpersonateUserExtraHeaderPrefix+k, v) + } + } + return rt.delegate.RoundTrip(req) } diff --git a/pkg/client/transport/round_trippers_test.go b/pkg/client/transport/round_trippers_test.go index 78af9a59eaf..85ea12e0a60 100644 --- a/pkg/client/transport/round_trippers_test.go +++ b/pkg/client/transport/round_trippers_test.go @@ -18,6 +18,7 @@ package transport import ( "net/http" + "reflect" "testing" ) @@ -99,3 +100,58 @@ func TestUserAgentRoundTripper(t *testing.T) { t.Errorf("unexpected user agent header: %#v", rt.Request) } } + +func TestImpersonationRoundTripper(t *testing.T) { + tcs := []struct { + name string + impersonationConfig ImpersonationConfig + expected map[string][]string + }{ + { + name: "all", + impersonationConfig: ImpersonationConfig{ + UserName: "user", + Groups: []string{"one", "two"}, + Extra: map[string][]string{ + "first": {"A", "a"}, + "second": {"B", "b"}, + }, + }, + expected: map[string][]string{ + ImpersonateUserHeader: {"user"}, + ImpersonateGroupHeader: {"one", "two"}, + ImpersonateUserExtraHeaderPrefix + "First": {"A", "a"}, + ImpersonateUserExtraHeaderPrefix + "Second": {"B", "b"}, + }, + }, + } + + for _, tc := range tcs { + rt := &testRoundTripper{} + req := &http.Request{ + Header: make(http.Header), + } + NewImpersonatingRoundTripper(tc.impersonationConfig, rt).RoundTrip(req) + + for k, v := range rt.Request.Header { + expected, ok := tc.expected[k] + if !ok { + t.Errorf("%v missing %v=%v", tc.name, k, v) + continue + } + if !reflect.DeepEqual(expected, v) { + t.Errorf("%v expected %v: %v, got %v", tc.name, k, expected, v) + } + } + for k, v := range tc.expected { + expected, ok := rt.Request.Header[k] + if !ok { + t.Errorf("%v missing %v=%v", tc.name, k, v) + continue + } + if !reflect.DeepEqual(expected, v) { + t.Errorf("%v expected %v: %v, got %v", tc.name, k, expected, v) + } + } + } +} diff --git a/pkg/client/unversioned/clientcmd/client_config.go b/pkg/client/unversioned/clientcmd/client_config.go index aa9996d8b85..72850438ee3 100644 --- a/pkg/client/unversioned/clientcmd/client_config.go +++ b/pkg/client/unversioned/clientcmd/client_config.go @@ -144,7 +144,7 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) { clientConfig.Host = u.String() } if len(configAuthInfo.Impersonate) > 0 { - clientConfig.Impersonate = configAuthInfo.Impersonate + clientConfig.Impersonate = restclient.ImpersonationConfig{UserName: configAuthInfo.Impersonate} } // only try to read the auth information if we are secure @@ -215,7 +215,7 @@ func getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fa mergedConfig.BearerToken = string(tokenBytes) } if len(configAuthInfo.Impersonate) > 0 { - mergedConfig.Impersonate = configAuthInfo.Impersonate + mergedConfig.Impersonate = restclient.ImpersonationConfig{UserName: configAuthInfo.Impersonate} } if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 { mergedConfig.CertFile = configAuthInfo.ClientCertificate