Skip to content

Commit

Permalink
--as-uid flag in kubectl and kubeconfigs.
Browse files Browse the repository at this point in the history
This corresponds to previous work to allow impersonating UIDs:
* Introduce Impersonate-UID header: kubernetes#99961
* Add UID to client-go impersonation config kubernetes#104483

Signed-off-by: Margo Crawford <margaretc@vmware.com>
  • Loading branch information
margocrawf committed Nov 5, 2021
1 parent 6044cdb commit 7e079f5
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 6 deletions.
Expand Up @@ -48,6 +48,7 @@ const (
flagCAFile = "certificate-authority"
flagBearerToken = "token"
flagImpersonate = "as"
flagImpersonateUID = "as-uid"
flagImpersonateGroup = "as-group"
flagUsername = "username"
flagPassword = "password"
Expand Down Expand Up @@ -94,6 +95,7 @@ type ConfigFlags struct {
CAFile *string
BearerToken *string
Impersonate *string
ImpersonateUID *string
ImpersonateGroup *[]string
Username *string
Password *string
Expand Down Expand Up @@ -171,6 +173,9 @@ func (f *ConfigFlags) toRawKubeConfigLoader() clientcmd.ClientConfig {
if f.Impersonate != nil {
overrides.AuthInfo.Impersonate = *f.Impersonate
}
if f.ImpersonateUID != nil {
overrides.AuthInfo.ImpersonateUID = *f.ImpersonateUID
}
if f.ImpersonateGroup != nil {
overrides.AuthInfo.ImpersonateGroups = *f.ImpersonateGroup
}
Expand Down Expand Up @@ -336,6 +341,9 @@ func (f *ConfigFlags) AddFlags(flags *pflag.FlagSet) {
if f.Impersonate != nil {
flags.StringVar(f.Impersonate, flagImpersonate, *f.Impersonate, "Username to impersonate for the operation. User could be a regular user or a service account in a namespace.")
}
if f.ImpersonateUID != nil {
flags.StringVar(f.ImpersonateUID, flagImpersonateUID, *f.ImpersonateUID, "UID to impersonate for the operation.")
}
if f.ImpersonateGroup != nil {
flags.StringArrayVar(f.ImpersonateGroup, flagImpersonateGroup, *f.ImpersonateGroup, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups.")
}
Expand Down Expand Up @@ -410,6 +418,7 @@ func NewConfigFlags(usePersistentConfig bool) *ConfigFlags {
CAFile: stringptr(""),
BearerToken: stringptr(""),
Impersonate: stringptr(""),
ImpersonateUID: stringptr(""),
ImpersonateGroup: &impersonateGroup,

usePersistentConfig: usePersistentConfig,
Expand Down
5 changes: 4 additions & 1 deletion staging/src/k8s.io/client-go/tools/clientcmd/api/types.go
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go
Expand Up @@ -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.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions staging/src/k8s.io/client-go/tools/clientcmd/client_config.go
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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,
}
Expand Down
43 changes: 43 additions & 0 deletions staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go
Expand Up @@ -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"

Expand Down Expand Up @@ -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{
Expand Down
4 changes: 4 additions & 0 deletions staging/src/k8s.io/client-go/tools/clientcmd/overrides.go
Expand Up @@ -53,6 +53,7 @@ type AuthOverrideFlags struct {
ClientKey FlagInfo
Token FlagInfo
Impersonate FlagInfo
ImpersonateUID FlagInfo
ImpersonateGroups FlagInfo
Username FlagInfo
Password FlagInfo
Expand Down Expand Up @@ -154,6 +155,7 @@ const (
FlagEmbedCerts = "embed-certs"
FlagBearerToken = "token"
FlagImpersonate = "as"
FlagImpersonateUID = "as-uid"
FlagImpersonateGroup = "as-group"
FlagUsername = "username"
FlagPassword = "password"
Expand All @@ -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"},
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions staging/src/k8s.io/client-go/tools/clientcmd/validation.go
Expand Up @@ -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
}
Expand Down
72 changes: 72 additions & 0 deletions staging/src/k8s.io/client-go/tools/clientcmd/validation_test.go
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions test/cmd/authorization.sh
Expand Up @@ -51,6 +51,9 @@ run_impersonation_tests() {
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'

output_message=$(! kubectl get pods "${kube_flags_with_token[@]:?}" --as-uid=abc123 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
Expand All @@ -63,6 +66,13 @@ run_impersonation_tests() {
kube::test::get_object_assert 'csr/foo' '{{len .spec.groups}}' '4'
kube::test::get_object_assert 'csr/foo' '{{range .spec.groups}}{{.}} {{end}}' 'group2 group1 ,,,chameleon system:authenticated '
kubectl delete -f hack/testdata/csr.yml "${kube_flags_with_token[@]:?}"

# --as-uid
kubectl create -f hack/testdata/csr.yml "${kube_flags_with_token[@]:?}" --as=user1 --as-uid=abc123
kube::test::get_object_assert 'csr/foo' '{{.spec.username}}' 'user1'
kube::test::get_object_assert 'csr/foo' '{{.spec.uid}}' 'abc123'
kubectl delete -f hack/testdata/csr.yml "${kube_flags_with_token[@]:?}"

fi

set +o nounset
Expand Down
47 changes: 47 additions & 0 deletions test/cmd/kubeconfig.sh
Expand Up @@ -249,6 +249,53 @@ run_client_config_tests() {
output_message=$(! kubectl get pod --kubeconfig=missing-config 2>&1)
kube::test::if_has_string "${output_message}" 'no such file or directory'

set +o nounset
set +o errexit
}

run_kubeconfig_impersonate_tests() {
set -o nounset
set -o errexit

kube::log::status "Testing config with impersonation"

# copy the existing kubeconfig over and add a new user entry for the admin user impersonating a different user.
kubectl config view --raw > "${TMPDIR:-/tmp}"/impersonateconfig.yaml
cat << EOF >> "${TMPDIR:-/tmp}"/impersonateconfig.yaml
users:
- name: admin-as-userb
user:
# token defined in hack/testdata/auth-tokens.csv
token: admin-token
# impersonated user
as: userb
as-uid: abc123
as-groups:
- group2
- group1
as-user-extra:
foo:
- bar
- baz
- name: as-uid-without-as
user:
# token defined in hack/testdata/auth-tokens.csv
token: admin-token
# impersonated uid
as-uid: abc123
EOF

kubectl create -f hack/testdata/csr.yml --kubeconfig "${TMPDIR:-/tmp}"/impersonateconfig.yaml --user admin-as-userb
kube::test::get_object_assert 'csr/foo' '{{.spec.username}}' 'userb'
kube::test::get_object_assert 'csr/foo' '{{.spec.uid}}' 'abc123'
kube::test::get_object_assert 'csr/foo' '{{range .spec.groups}}{{.}} {{end}}' 'group2 group1 system:authenticated '
kube::test::get_object_assert 'csr/foo' '{{len .spec.extra}}' '1'
kube::test::get_object_assert 'csr/foo' '{{range .spec.extra.foo}}{{.}} {{end}}' 'bar baz '
kubectl delete -f hack/testdata/csr.yml

output_message=$(! kubectl get pods --kubeconfig "${TMPDIR:-/tmp}"/impersonateconfig.yaml --user as-uid-without-as 2>&1)
kube::test::if_has_string "${output_message}" 'without impersonating a user'

set +o nounset
set +o errexit
}

0 comments on commit 7e079f5

Please sign in to comment.