diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index ac2cbb51b3f..234d9aa02fa 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -28,6 +28,7 @@ KUBE_ROOT=$(dirname "${BASH_SOURCE}")/../.. ETCD_HOST=${ETCD_HOST:-127.0.0.1} ETCD_PORT=${ETCD_PORT:-2379} API_PORT=${API_PORT:-8080} +SECURE_API_PORT=${SECURE_API_PORT:-6443} API_HOST=${API_HOST:-127.0.0.1} KUBELET_PORT=${KUBELET_PORT:-10250} KUBELET_HEALTHZ_PORT=${KUBELET_HEALTHZ_PORT:-10248} @@ -2855,8 +2856,14 @@ runTests() { kube_flags=( -s "http://127.0.0.1:${API_PORT}" ) + + kube_flags_with_token=( + -s "https://127.0.0.1:${SECURE_API_PORT}" --token=admin/system:masters --insecure-skip-tls-verify=true + ) + if [[ -z "${ALLOW_SKEW:-}" ]]; then kube_flags+=("--match-server-version") + kube_flags_with_token+=("--match-server-version") fi if kube::test::if_supports_resource "${nodes}" ; then [ "$(kubectl get nodes -o go-template='{{ .apiVersion }}' "${kube_flags[@]}")" == "v1" ] @@ -3762,5 +3769,25 @@ __EOF__ output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/ kubectl plugin error 2>&1) kube::test::if_has_string "${output_message}" 'error: exit status 1' + ################# + # Impersonation # + ################# + output_message=$(! kubectl get pods "${kube_flags_with_token[@]}" --as-group=foo 2>&1) + kube::test::if_has_string "${output_message}" 'without impersonating a user' + + if kube::test::if_supports_resource "${csr}" ; then + # --as + kubectl create -f hack/testdata/csr.yml "${kube_flags_with_token[@]}" --as=user1 + kube::test::get_object_assert 'csr/foo' '{{.spec.username}}' 'user1' + kube::test::get_object_assert 'csr/foo' '{{range .spec.groups}}{{.}}{{end}}' 'system:authenticated' + kubectl delete -f hack/testdata/csr.yml "${kube_flags_with_token[@]}" + + # --as-group + kubectl create -f hack/testdata/csr.yml "${kube_flags_with_token[@]}" --as=user1 --as-group=group2 --as-group=group1 --as-group=,,,chameleon + kube::test::get_object_assert 'csr/foo' '{{len .spec.groups}}' '3' + kube::test::get_object_assert 'csr/foo' '{{range .spec.groups}}{{.}} {{end}}' 'group2 group1 ,,,chameleon ' + kubectl delete -f hack/testdata/csr.yml "${kube_flags_with_token[@]}" + fi + kube::test::clear_all } diff --git a/hack/make-rules/test-cmd.sh b/hack/make-rules/test-cmd.sh index 430cfdf6b3e..09f8a3abc34 100755 --- a/hack/make-rules/test-cmd.sh +++ b/hack/make-rules/test-cmd.sh @@ -44,6 +44,7 @@ function run_kube_apiserver() { --public-address-override="127.0.0.1" \ --port="${API_PORT}" \ --authorization-mode="${AUTHORIZATION_MODE}" \ + --secure-port="${SECURE_API_PORT}" \ --admission-control="${ADMISSION_CONTROL}" \ --etcd-servers="http://${ETCD_HOST}:${ETCD_PORT}" \ --public-address-override="127.0.0.1" \ @@ -51,7 +52,8 @@ function run_kube_apiserver() { --runtime-config=api/v1 \ --storage-media-type="${KUBE_TEST_API_STORAGE_TYPE-}" \ --cert-dir="${TMPDIR:-/tmp/}" \ - --service-cluster-ip-range="10.0.0.0/24" 1>&2 & + --service-cluster-ip-range="10.0.0.0/24" \ + --insecure-allow-any-token 1>&2 & APISERVER_PID=$! kube::util::wait_for_url "http://127.0.0.1:${API_PORT}/healthz" "apiserver" diff --git a/hack/make-rules/test-federation-cmd.sh b/hack/make-rules/test-federation-cmd.sh index 8446f7eff59..4fdec716e3f 100755 --- a/hack/make-rules/test-federation-cmd.sh +++ b/hack/make-rules/test-federation-cmd.sh @@ -38,10 +38,12 @@ function run_federation_apiserver() { "${KUBE_OUTPUT_HOSTBIN}/federation-apiserver" \ --insecure-port="${API_PORT}" \ + --secure-port="${SECURE_API_PORT}" \ --admission-control="${ADMISSION_CONTROL}" \ --etcd-servers="http://${ETCD_HOST}:${ETCD_PORT}" \ --storage-media-type="${KUBE_TEST_API_STORAGE_TYPE-}" \ - --cert-dir="${TMPDIR:-/tmp/}" 1>&2 & + --cert-dir="${TMPDIR:-/tmp/}" \ + --insecure-allow-any-token 1>&2 & APISERVER_PID=$! kube::util::wait_for_url "http://127.0.0.1:${API_PORT}/healthz" "apiserver" diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go index d52c7c948e6..11a5e7a2774 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go @@ -109,6 +109,12 @@ type AuthInfo struct { // Impersonate is the username to act-as. // +optional Impersonate string `json:"act-as,omitempty"` + // ImpersonateGroups is the groups to imperonate. + // +optional + ImpersonateGroups []string `json:"act-as-groups,omitempty"` + // ImpersonateUserExtra contains additional information for impersonated user. + // +optional + ImpersonateUserExtra map[string][]string `json:"act-as-user-extra,omitempty"` // Username is the username for basic authentication to the kubernetes cluster. // +optional Username string `json:"username,omitempty"` @@ -172,7 +178,10 @@ func NewCluster() *Cluster { // NewAuthInfo is a convenience function that returns a new AuthInfo // object with non-nil maps func NewAuthInfo() *AuthInfo { - return &AuthInfo{Extensions: make(map[string]runtime.Object)} + return &AuthInfo{ + Extensions: make(map[string]runtime.Object), + ImpersonateUserExtra: make(map[string][]string), + } } // NewPreferences is a convenience function that returns a new diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go index a0777ba53e1..bad2f6ac2ec 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go @@ -103,6 +103,12 @@ type AuthInfo struct { // Impersonate is the username to imperonate. The name matches the flag. // +optional Impersonate string `json:"as,omitempty"` + // ImpersonateGroups is the groups to imperonate. + // +optional + ImpersonateGroups []string `json:"as-groups,omitempty"` + // ImpersonateUserExtra contains additional information for impersonated user. + // +optional + ImpersonateUserExtra map[string][]string `json:"as-user-extra,omitempty"` // Username is the username for basic authentication to the kubernetes cluster. // +optional Username string `json:"username,omitempty"` diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/client_config.go b/staging/src/k8s.io/client-go/tools/clientcmd/client_config.go index c5c35307fa2..b622c6dc4a1 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/client_config.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/client_config.go @@ -146,7 +146,11 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) { clientConfig.Host = u.String() } if len(configAuthInfo.Impersonate) > 0 { - clientConfig.Impersonate = restclient.ImpersonationConfig{UserName: configAuthInfo.Impersonate} + clientConfig.Impersonate = restclient.ImpersonationConfig{ + UserName: configAuthInfo.Impersonate, + Groups: configAuthInfo.ImpersonateGroups, + Extra: configAuthInfo.ImpersonateUserExtra, + } } // only try to read the auth information if we are secure @@ -217,7 +221,11 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI mergedConfig.BearerToken = string(tokenBytes) } if len(configAuthInfo.Impersonate) > 0 { - mergedConfig.Impersonate = restclient.ImpersonationConfig{UserName: configAuthInfo.Impersonate} + mergedConfig.Impersonate = restclient.ImpersonationConfig{ + UserName: configAuthInfo.Impersonate, + Groups: configAuthInfo.ImpersonateGroups, + Extra: configAuthInfo.ImpersonateUserExtra, + } } if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 { mergedConfig.CertFile = configAuthInfo.ClientCertificate diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/overrides.go b/staging/src/k8s.io/client-go/tools/clientcmd/overrides.go index 9ab8b65407e..bd817bbf5f3 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/overrides.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/overrides.go @@ -52,6 +52,7 @@ type AuthOverrideFlags struct { ClientKey FlagInfo Token FlagInfo Impersonate FlagInfo + ImpersonateGroups FlagInfo Username FlagInfo Password FlagInfo } @@ -100,6 +101,19 @@ func (f FlagInfo) BindStringFlag(flags *pflag.FlagSet, target *string) FlagInfo return f } +// BindStringSliceFlag binds the flag based on the provided info. If LongName == "", nothing is registered +func (f FlagInfo) BindStringArrayFlag(flags *pflag.FlagSet, target *[]string) FlagInfo { + // you can't register a flag without a long name + if len(f.LongName) > 0 { + sliceVal := []string{} + if len(f.Default) > 0 { + sliceVal = []string{f.Default} + } + flags.StringArrayVarP(target, f.LongName, f.ShortName, sliceVal, f.Description) + } + return f +} + // BindBoolFlag binds the flag based on the provided info. If LongName == "", nothing is registered func (f FlagInfo) BindBoolFlag(flags *pflag.FlagSet, target *bool) FlagInfo { // you can't register a flag without a long name @@ -116,22 +130,23 @@ func (f FlagInfo) BindBoolFlag(flags *pflag.FlagSet, target *bool) FlagInfo { } const ( - FlagClusterName = "cluster" - FlagAuthInfoName = "user" - FlagContext = "context" - FlagNamespace = "namespace" - FlagAPIServer = "server" - FlagAPIVersion = "api-version" - FlagInsecure = "insecure-skip-tls-verify" - FlagCertFile = "client-certificate" - FlagKeyFile = "client-key" - FlagCAFile = "certificate-authority" - FlagEmbedCerts = "embed-certs" - FlagBearerToken = "token" - FlagImpersonate = "as" - FlagUsername = "username" - FlagPassword = "password" - FlagTimeout = "request-timeout" + FlagClusterName = "cluster" + FlagAuthInfoName = "user" + FlagContext = "context" + FlagNamespace = "namespace" + FlagAPIServer = "server" + FlagAPIVersion = "api-version" + FlagInsecure = "insecure-skip-tls-verify" + FlagCertFile = "client-certificate" + FlagKeyFile = "client-key" + FlagCAFile = "certificate-authority" + FlagEmbedCerts = "embed-certs" + FlagBearerToken = "token" + FlagImpersonate = "as" + FlagImpersonateGroup = "as-group" + FlagUsername = "username" + FlagPassword = "password" + FlagTimeout = "request-timeout" ) // RecommendedConfigOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing @@ -153,6 +168,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"}, + 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"}, } @@ -192,6 +208,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.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/staging/src/k8s.io/client-go/tools/clientcmd/validation.go b/staging/src/k8s.io/client-go/tools/clientcmd/validation.go index 18cf2680a55..2bae0c395d2 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/validation.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/validation.go @@ -242,6 +242,10 @@ 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)) + } return validationErrors }