From 5fca705b7d23975809429ad852f296f3ec1cc6ed Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 19 Oct 2021 16:12:31 -0700 Subject: [PATCH] --as-uid flag in kubectl and kubeconfigs. This corresponds to previous work to allow impersonating UIDs: * Introduce Impersonate-UID header: #99961 * Add UID to client-go impersonation config #104483 Signed-off-by: Margo Crawford Kubernetes-commit: 7e079f5144474cae05802771f604df2b748d781f --- tools/clientcmd/api/types.go | 5 +- tools/clientcmd/api/v1/types.go | 7 +- .../api/v1/zz_generated.conversion.go | 2 + tools/clientcmd/client_config.go | 2 + tools/clientcmd/client_config_test.go | 43 +++++++++++ tools/clientcmd/overrides.go | 4 ++ tools/clientcmd/validation.go | 6 +- tools/clientcmd/validation_test.go | 72 +++++++++++++++++++ 8 files changed, 135 insertions(+), 6 deletions(-) diff --git a/tools/clientcmd/api/types.go b/tools/clientcmd/api/types.go index 31716abf..44a2fb94 100644 --- a/tools/clientcmd/api/types.go +++ b/tools/clientcmd/api/types.go @@ -124,7 +124,10 @@ type AuthInfo struct { // Impersonate is the username to act-as. // +optional Impersonate string `json:"act-as,omitempty"` - // ImpersonateGroups is the groups to imperonate. + // ImpersonateUID is the uid to impersonate. + // +optional + ImpersonateUID string `json:"act-as-uid,omitempty"` + // ImpersonateGroups is the groups to impersonate. // +optional ImpersonateGroups []string `json:"act-as-groups,omitempty"` // ImpersonateUserExtra contains additional information for impersonated user. diff --git a/tools/clientcmd/api/v1/types.go b/tools/clientcmd/api/v1/types.go index 7e835103..757ed817 100644 --- a/tools/clientcmd/api/v1/types.go +++ b/tools/clientcmd/api/v1/types.go @@ -111,10 +111,13 @@ type AuthInfo struct { // TokenFile is a pointer to a file that contains a bearer token (as described above). If both Token and TokenFile are present, Token takes precedence. // +optional TokenFile string `json:"tokenFile,omitempty"` - // Impersonate is the username to imperonate. The name matches the flag. + // Impersonate is the username to impersonate. The name matches the flag. // +optional Impersonate string `json:"as,omitempty"` - // ImpersonateGroups is the groups to imperonate. + // ImpersonateUID is the uid to impersonate. + // +optional + ImpersonateUID string `json:"as-uid,omitempty"` + // ImpersonateGroups is the groups to impersonate. // +optional ImpersonateGroups []string `json:"as-groups,omitempty"` // ImpersonateUserExtra contains additional information for impersonated user. diff --git a/tools/clientcmd/api/v1/zz_generated.conversion.go b/tools/clientcmd/api/v1/zz_generated.conversion.go index 1e060654..a13bae64 100644 --- a/tools/clientcmd/api/v1/zz_generated.conversion.go +++ b/tools/clientcmd/api/v1/zz_generated.conversion.go @@ -167,6 +167,7 @@ func autoConvert_v1_AuthInfo_To_api_AuthInfo(in *AuthInfo, out *api.AuthInfo, s out.Token = in.Token out.TokenFile = in.TokenFile out.Impersonate = in.Impersonate + out.ImpersonateUID = in.ImpersonateUID out.ImpersonateGroups = *(*[]string)(unsafe.Pointer(&in.ImpersonateGroups)) out.ImpersonateUserExtra = *(*map[string][]string)(unsafe.Pointer(&in.ImpersonateUserExtra)) out.Username = in.Username @@ -201,6 +202,7 @@ func autoConvert_api_AuthInfo_To_v1_AuthInfo(in *api.AuthInfo, out *AuthInfo, s out.Token = in.Token out.TokenFile = in.TokenFile out.Impersonate = in.Impersonate + out.ImpersonateUID = in.ImpersonateUID out.ImpersonateGroups = *(*[]string)(unsafe.Pointer(&in.ImpersonateGroups)) out.ImpersonateUserExtra = *(*map[string][]string)(unsafe.Pointer(&in.ImpersonateUserExtra)) out.Username = in.Username diff --git a/tools/clientcmd/client_config.go b/tools/clientcmd/client_config.go index 0a905490..cc37c9fb 100644 --- a/tools/clientcmd/client_config.go +++ b/tools/clientcmd/client_config.go @@ -181,6 +181,7 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) { if len(configAuthInfo.Impersonate) > 0 { clientConfig.Impersonate = restclient.ImpersonationConfig{ UserName: configAuthInfo.Impersonate, + UID: configAuthInfo.ImpersonateUID, Groups: configAuthInfo.ImpersonateGroups, Extra: configAuthInfo.ImpersonateUserExtra, } @@ -255,6 +256,7 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI if len(configAuthInfo.Impersonate) > 0 { mergedConfig.Impersonate = restclient.ImpersonationConfig{ UserName: configAuthInfo.Impersonate, + UID: configAuthInfo.ImpersonateUID, Groups: configAuthInfo.ImpersonateGroups, Extra: configAuthInfo.ImpersonateUserExtra, } diff --git a/tools/clientcmd/client_config_test.go b/tools/clientcmd/client_config_test.go index a770dd61..6af32458 100644 --- a/tools/clientcmd/client_config_test.go +++ b/tools/clientcmd/client_config_test.go @@ -217,6 +217,43 @@ func TestTLSServerNameClearsWhenServerNameSet(t *testing.T) { matchStringArg("", actualCfg.ServerName, t) } +func TestFullImpersonateConfig(t *testing.T) { + config := createValidTestConfig() + config.Clusters["clean"] = &clientcmdapi.Cluster{ + Server: "https://localhost:8443", + } + config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{ + Impersonate: "alice", + ImpersonateUID: "abc123", + ImpersonateGroups: []string{"group-1"}, + ImpersonateUserExtra: map[string][]string{"some-key": {"some-value"}}, + } + config.Contexts["clean"] = &clientcmdapi.Context{ + Cluster: "clean", + AuthInfo: "clean", + } + config.CurrentContext = "clean" + + clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{ + ClusterInfo: clientcmdapi.Cluster{ + Server: "http://something", + }, + }, nil) + + actualCfg, err := clientBuilder.ClientConfig() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + matchStringArg("alice", actualCfg.Impersonate.UserName, t) + matchStringArg("abc123", actualCfg.Impersonate.UID, t) + matchIntArg(1, len(actualCfg.Impersonate.Groups), t) + matchStringArg("group-1", actualCfg.Impersonate.Groups[0], t) + matchIntArg(1, len(actualCfg.Impersonate.Extra), t) + matchIntArg(1, len(actualCfg.Impersonate.Extra["some-key"]), t) + matchStringArg("some-value", actualCfg.Impersonate.Extra["some-key"][0], t) +} + func TestMergeContext(t *testing.T) { const namespace = "overridden-namespace" @@ -808,6 +845,12 @@ func matchByteArg(expected, got []byte, t *testing.T) { } } +func matchIntArg(expected, got int, t *testing.T) { + if expected != got { + t.Errorf("Expected %d, got %d", expected, got) + } +} + func TestNamespaceOverride(t *testing.T) { config := &DirectClientConfig{ overrides: &ConfigOverrides{ diff --git a/tools/clientcmd/overrides.go b/tools/clientcmd/overrides.go index 95cba0fa..ff643cc1 100644 --- a/tools/clientcmd/overrides.go +++ b/tools/clientcmd/overrides.go @@ -53,6 +53,7 @@ type AuthOverrideFlags struct { ClientKey FlagInfo Token FlagInfo Impersonate FlagInfo + ImpersonateUID FlagInfo ImpersonateGroups FlagInfo Username FlagInfo Password FlagInfo @@ -154,6 +155,7 @@ const ( FlagEmbedCerts = "embed-certs" FlagBearerToken = "token" FlagImpersonate = "as" + FlagImpersonateUID = "as-uid" FlagImpersonateGroup = "as-group" FlagUsername = "username" FlagPassword = "password" @@ -179,6 +181,7 @@ func RecommendedAuthOverrideFlags(prefix string) AuthOverrideFlags { ClientKey: FlagInfo{prefix + FlagKeyFile, "", "", "Path to a client key file for TLS"}, Token: FlagInfo{prefix + FlagBearerToken, "", "", "Bearer token for authentication to the API server"}, Impersonate: FlagInfo{prefix + FlagImpersonate, "", "", "Username to impersonate for the operation"}, + ImpersonateUID: FlagInfo{prefix + FlagImpersonateUID, "", "", "UID to impersonate for the operation"}, ImpersonateGroups: FlagInfo{prefix + FlagImpersonateGroup, "", "", "Group to impersonate for the operation, this flag can be repeated to specify multiple groups."}, Username: FlagInfo{prefix + FlagUsername, "", "", "Username for basic authentication to the API server"}, Password: FlagInfo{prefix + FlagPassword, "", "", "Password for basic authentication to the API server"}, @@ -219,6 +222,7 @@ func BindAuthInfoFlags(authInfo *clientcmdapi.AuthInfo, flags *pflag.FlagSet, fl flagNames.ClientKey.BindStringFlag(flags, &authInfo.ClientKey).AddSecretAnnotation(flags) flagNames.Token.BindStringFlag(flags, &authInfo.Token).AddSecretAnnotation(flags) flagNames.Impersonate.BindStringFlag(flags, &authInfo.Impersonate).AddSecretAnnotation(flags) + flagNames.ImpersonateUID.BindStringFlag(flags, &authInfo.ImpersonateUID).AddSecretAnnotation(flags) flagNames.ImpersonateGroups.BindStringArrayFlag(flags, &authInfo.ImpersonateGroups).AddSecretAnnotation(flags) flagNames.Username.BindStringFlag(flags, &authInfo.Username).AddSecretAnnotation(flags) flagNames.Password.BindStringFlag(flags, &authInfo.Password).AddSecretAnnotation(flags) diff --git a/tools/clientcmd/validation.go b/tools/clientcmd/validation.go index 10c3c6d5..2ae1eb70 100644 --- a/tools/clientcmd/validation.go +++ b/tools/clientcmd/validation.go @@ -323,9 +323,9 @@ func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []err validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods)) } - // ImpersonateGroups or ImpersonateUserExtra should be requested with a user - if (len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) { - validationErrors = append(validationErrors, fmt.Errorf("requesting groups or user-extra for %v without impersonating a user", authInfoName)) + // ImpersonateUID, ImpersonateGroups or ImpersonateUserExtra should be requested with a user + if (len(authInfo.ImpersonateUID) > 0 || len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) { + validationErrors = append(validationErrors, fmt.Errorf("requesting uid, groups or user-extra for %v without impersonating a user", authInfoName)) } return validationErrors } diff --git a/tools/clientcmd/validation_test.go b/tools/clientcmd/validation_test.go index 676d1989..bf72c6c0 100644 --- a/tools/clientcmd/validation_test.go +++ b/tools/clientcmd/validation_test.go @@ -508,6 +508,78 @@ func TestValidateAuthInfoExecInteractiveModeInvalid(t *testing.T) { test.testConfig(t) } +func TestValidateAuthInfoImpersonateUser(t *testing.T) { + config := clientcmdapi.NewConfig() + config.AuthInfos["user"] = &clientcmdapi.AuthInfo{ + Impersonate: "user", + } + test := configValidationTest{ + config: config, + } + test.testAuthInfo("user", t) + test.testConfig(t) +} + +func TestValidateAuthInfoImpersonateEverything(t *testing.T) { + config := clientcmdapi.NewConfig() + config.AuthInfos["user"] = &clientcmdapi.AuthInfo{ + Impersonate: "user", + ImpersonateUID: "abc123", + ImpersonateGroups: []string{"group-1", "group-2"}, + ImpersonateUserExtra: map[string][]string{"key": {"val1", "val2"}}, + } + test := configValidationTest{ + config: config, + } + test.testAuthInfo("user", t) + test.testConfig(t) +} + +func TestValidateAuthInfoImpersonateGroupsWithoutUserInvalid(t *testing.T) { + config := clientcmdapi.NewConfig() + config.AuthInfos["user"] = &clientcmdapi.AuthInfo{ + ImpersonateGroups: []string{"group-1", "group-2"}, + } + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{ + `requesting uid, groups or user-extra for user without impersonating a user`, + }, + } + test.testAuthInfo("user", t) + test.testConfig(t) +} + +func TestValidateAuthInfoImpersonateExtraWithoutUserInvalid(t *testing.T) { + config := clientcmdapi.NewConfig() + config.AuthInfos["user"] = &clientcmdapi.AuthInfo{ + ImpersonateUserExtra: map[string][]string{"key": {"val1", "val2"}}, + } + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{ + `requesting uid, groups or user-extra for user without impersonating a user`, + }, + } + test.testAuthInfo("user", t) + test.testConfig(t) +} + +func TestValidateAuthInfoImpersonateUIDWithoutUserInvalid(t *testing.T) { + config := clientcmdapi.NewConfig() + config.AuthInfos["user"] = &clientcmdapi.AuthInfo{ + ImpersonateUID: "abc123", + } + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{ + `requesting uid, groups or user-extra for user without impersonating a user`, + }, + } + test.testAuthInfo("user", t) + test.testConfig(t) +} + type configValidationTest struct { config *clientcmdapi.Config expectedErrorSubstring []string