diff --git a/cmd/kube-apiserver/app/BUILD b/cmd/kube-apiserver/app/BUILD index d8c7a47cf09..96fa895cbfe 100644 --- a/cmd/kube-apiserver/app/BUILD +++ b/cmd/kube-apiserver/app/BUILD @@ -20,6 +20,7 @@ go_library( "//pkg/apis/batch:go_default_library", "//pkg/capabilities:go_default_library", "//pkg/client/clientset_generated/internalclientset:go_default_library", + "//pkg/client/informers/informers_generated/internalversion:go_default_library", "//pkg/cloudprovider:go_default_library", "//pkg/cloudprovider/providers:go_default_library", "//pkg/controller/informers:go_default_library", @@ -51,10 +52,12 @@ go_library( "//plugin/pkg/admission/securitycontext/scdeny:go_default_library", "//plugin/pkg/admission/serviceaccount:go_default_library", "//plugin/pkg/admission/storageclass/default:go_default_library", + "//plugin/pkg/auth/authenticator/token/bootstrap:go_default_library", "//vendor:github.com/go-openapi/spec", "//vendor:github.com/golang/glog", "//vendor:github.com/spf13/cobra", "//vendor:github.com/spf13/pflag", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/openapi", "//vendor:k8s.io/apimachinery/pkg/runtime/schema", "//vendor:k8s.io/apimachinery/pkg/util/errors", diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 6d74badd1c3..fcc5a6ea784 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -35,6 +35,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/openapi" "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -50,6 +51,7 @@ import ( "k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/capabilities" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" "k8s.io/kubernetes/pkg/cloudprovider" "k8s.io/kubernetes/pkg/controller/informers" serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount" @@ -61,6 +63,7 @@ import ( "k8s.io/kubernetes/pkg/master/tunneler" "k8s.io/kubernetes/pkg/registry/cachesize" "k8s.io/kubernetes/pkg/version" + "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap" ) // NewAPIServerCommand creates a *cobra.Command object with default parameters @@ -253,11 +256,6 @@ func Run(s *options.ServerRunOptions) error { authenticatorConfig.ServiceAccountTokenGetter = serviceaccountcontroller.NewGetterFromStorageInterface(storageConfig, storageFactory.ResourcePrefix(api.Resource("serviceaccounts")), storageFactory.ResourcePrefix(api.Resource("secrets"))) } - apiAuthenticator, securityDefinitions, err := authenticatorConfig.New() - if err != nil { - return fmt.Errorf("invalid Authentication Config: %v", err) - } - client, err := internalclientset.NewForConfig(genericConfig.LoopbackClientConfig) if err != nil { kubeAPIVersions := os.Getenv("KUBE_API_VERSIONS") @@ -267,10 +265,29 @@ func Run(s *options.ServerRunOptions) error { // KUBE_API_VERSIONS is used in test-update-storage-objects.sh, disabling a number of API // groups. This leads to a nil client above and undefined behaviour further down. + // // TODO: get rid of KUBE_API_VERSIONS or define sane behaviour if set glog.Errorf("Failed to create clientset with KUBE_API_VERSIONS=%q. KUBE_API_VERSIONS is only for testing. Things will break.", kubeAPIVersions) } + + // TODO: Internal informers should switch to using 'pkg/client/informers/informers_generated', + // the second informer created here. Refactor clients which take the former to accept the latter. sharedInformers := informers.NewSharedInformerFactory(nil, client, 10*time.Minute) + internalSharedInformers := internalversion.NewSharedInformerFactory(client, 10*time.Minute) + + if client == nil { + // TODO: Remove check once client can never be nil. + glog.Errorf("Failed to setup bootstrap token authenticator because the loopback clientset was not setup properly.") + } else { + authenticatorConfig.BootstrapTokenAuthenticator = bootstrap.NewTokenAuthenticator( + internalSharedInformers.Core().InternalVersion().Secrets().Lister().Secrets(v1.NamespaceSystem), + ) + } + + apiAuthenticator, securityDefinitions, err := authenticatorConfig.New() + if err != nil { + return fmt.Errorf("invalid authentication config: %v", err) + } authorizationConfig := s.Authorization.ToAuthorizationConfig(sharedInformers) apiAuthorizer, err := authorizationConfig.New() @@ -350,6 +367,7 @@ func Run(s *options.ServerRunOptions) error { } sharedInformers.Start(wait.NeverStop) + internalSharedInformers.Start(wait.NeverStop) m.GenericAPIServer.PrepareRun().Run(wait.NeverStop) return nil } diff --git a/hack/.linted_packages b/hack/.linted_packages index 65ece82c7f8..3d83b3c9ed3 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -267,6 +267,7 @@ plugin/pkg/admission/resourcequota/apis/resourcequota/install plugin/pkg/admission/resourcequota/apis/resourcequota/validation plugin/pkg/admission/securitycontext/scdeny plugin/pkg/auth +plugin/pkg/auth/authenticator/token/bootstrap plugin/pkg/auth/authorizer plugin/pkg/auth/authorizer/rbac/bootstrappolicy staging/src/k8s.io/apimachinery/pkg/api/equality diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 74a9a76668c..1a18d7dd9aa 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -205,6 +205,7 @@ executor-suicide-timeout exit-on-lock-contention experimental-allowed-unsafe-sysctls experimental-bootstrap-kubeconfig +experimental-bootstrap-token-auth experimental-keystone-url experimental-keystone-ca-file experimental-mounter-path diff --git a/pkg/bootstrap/api/types.go b/pkg/bootstrap/api/types.go index 8a8dd1c7bd3..f25d197844d 100644 --- a/pkg/bootstrap/api/types.go +++ b/pkg/bootstrap/api/types.go @@ -33,12 +33,12 @@ const ( SecretTypeBootstrapToken v1.SecretType = "bootstrap.kubernetes.io/token" // BootstrapTokenIDKey is the id of this token. This can be transmitted in the - // clear and encoded in the name of the secret. It should be a random 6 - // character string. Required + // clear and encoded in the name of the secret. It must be a random 6 character + // string that matches the regexp `^([a-z0-9]{6})$`. Required. BootstrapTokenIDKey = "token-id" - // BootstrapTokenSecretKey is the actual secret. Typically this is a random 16 - // character string. Required. + // BootstrapTokenSecretKey is the actual secret. It must be a random 16 character + // string that matches the regexp `^([a-z0-9]{16})$`. Required. BootstrapTokenSecretKey = "token-secret" // BootstrapTokenExpirationKey is when this token should be expired and no @@ -52,6 +52,13 @@ const ( // other value is assumed to be false. Optional. BootstrapTokenUsageSigningKey = "usage-bootstrap-signing" + // BootstrapTokenUsageAuthentication signals that this token should be used + // as a bearer token to authenticate against the Kubernetes API. The bearer + // token takes the form "." and authenticates as the + // user "system:bootstrap:" in the group "system:bootstrappers". + // Value must be "true". Any other value is assumed to be false. Optional. + BootstrapTokenUsageAuthentication = "usage-bootstrap-authentication" + // ConfigMapClusterInfo defines the name for the ConfigMap where the information how to connect and trust the cluster exist ConfigMapClusterInfo = "cluster-info" @@ -60,4 +67,11 @@ const ( // JWSSignatureKeyPrefix defines what key prefix the JWS-signed tokens have JWSSignatureKeyPrefix = "jws-kubeconfig-" + + // BootstrapUserPrefix is the username prefix bootstrapping bearer tokens + // authenticate as. The full username given is "system:bootstrap:". + BootstrapUserPrefix = "system:bootstrap:" + + // BootstrapGroup is the group bootstrapping bearer tokens authenticate in. + BootstrapGroup = "system:bootstrappers" ) diff --git a/pkg/kubeapiserver/authenticator/config.go b/pkg/kubeapiserver/authenticator/config.go index b871d826fcb..d23cd62eb0f 100644 --- a/pkg/kubeapiserver/authenticator/config.go +++ b/pkg/kubeapiserver/authenticator/config.go @@ -49,6 +49,7 @@ type AuthenticatorConfig struct { Anonymous bool AnyToken bool BasicAuthFile string + BootstrapToken bool ClientCAFile string TokenAuthFile string OIDCIssuerURL string @@ -66,7 +67,8 @@ type AuthenticatorConfig struct { RequestHeaderConfig *authenticatorfactory.RequestHeaderConfig // TODO, this is the only non-serializable part of the entire config. Factor it out into a clientconfig - ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter + ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter + BootstrapTokenAuthenticator authenticator.Token } // New returns an authenticator.Request or an error that supports the standard @@ -136,6 +138,13 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe authenticators = append(authenticators, serviceAccountAuth) hasTokenAuth = true } + if config.BootstrapToken { + if config.BootstrapTokenAuthenticator != nil { + // TODO: This can sometimes be nil because of + authenticators = append(authenticators, bearertoken.New(config.BootstrapTokenAuthenticator)) + hasTokenAuth = true + } + } // NOTE(ericchiang): Keep the OpenID Connect after Service Accounts. // // Because both plugins verify JWTs whichever comes first in the union experiences diff --git a/pkg/kubeapiserver/options/authentication.go b/pkg/kubeapiserver/options/authentication.go index 51f3b9c7b05..da9c3fc1e79 100644 --- a/pkg/kubeapiserver/options/authentication.go +++ b/pkg/kubeapiserver/options/authentication.go @@ -33,6 +33,7 @@ import ( type BuiltInAuthenticationOptions struct { Anonymous *AnonymousAuthenticationOptions AnyToken *AnyTokenAuthenticationOptions + BootstrapToken *BootstrapTokenAuthenticationOptions ClientCert *genericoptions.ClientCertAuthenticationOptions Keystone *KeystoneAuthenticationOptions OIDC *OIDCAuthenticationOptions @@ -51,6 +52,10 @@ type AnonymousAuthenticationOptions struct { Allow bool } +type BootstrapTokenAuthenticationOptions struct { + Allow bool +} + type KeystoneAuthenticationOptions struct { URL string CAFile string @@ -90,6 +95,7 @@ func (s *BuiltInAuthenticationOptions) WithAll() *BuiltInAuthenticationOptions { return s. WithAnyonymous(). WithAnyToken(). + WithBootstrapToken(). WithClientCert(). WithKeystone(). WithOIDC(). @@ -110,6 +116,11 @@ func (s *BuiltInAuthenticationOptions) WithAnyToken() *BuiltInAuthenticationOpti return s } +func (s *BuiltInAuthenticationOptions) WithBootstrapToken() *BuiltInAuthenticationOptions { + s.BootstrapToken = &BootstrapTokenAuthenticationOptions{} + return s +} + func (s *BuiltInAuthenticationOptions) WithClientCert() *BuiltInAuthenticationOptions { s.ClientCert = &genericoptions.ClientCertAuthenticationOptions{} return s @@ -172,6 +183,12 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { } + if s.BootstrapToken != nil { + fs.BoolVar(&s.BootstrapToken.Allow, "experimental-bootstrap-token-auth", s.BootstrapToken.Allow, ""+ + "Enable to allow secrets of type 'bootstrap.kubernetes.io/token' in the 'kube-system' "+ + "namespace to be used for TLS bootstrapping authentication.") + } + if s.ClientCert != nil { s.ClientCert.AddFlags(fs) } @@ -255,6 +272,10 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig() authenticator.Au ret.AnyToken = s.AnyToken.Allow } + if s.BootstrapToken != nil { + ret.BootstrapToken = s.BootstrapToken.Allow + } + if s.ClientCert != nil { ret.ClientCAFile = s.ClientCert.ClientCA } diff --git a/plugin/pkg/auth/BUILD b/plugin/pkg/auth/BUILD index 3da6480b167..423becf165d 100644 --- a/plugin/pkg/auth/BUILD +++ b/plugin/pkg/auth/BUILD @@ -24,6 +24,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//plugin/pkg/auth/authenticator/token/bootstrap:all-srcs", "//plugin/pkg/auth/authorizer:all-srcs", ], tags = ["automanaged"], diff --git a/plugin/pkg/auth/authenticator/token/bootstrap/BUILD b/plugin/pkg/auth/authenticator/token/bootstrap/BUILD new file mode 100644 index 00000000000..7dc3587b8d0 --- /dev/null +++ b/plugin/pkg/auth/authenticator/token/bootstrap/BUILD @@ -0,0 +1,52 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = ["bootstrap_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/bootstrap/api:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/api/errors", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + "//vendor:k8s.io/apimachinery/pkg/labels", + "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + "//vendor:k8s.io/apiserver/pkg/authentication/user", + ], +) + +go_library( + name = "go_default_library", + srcs = ["bootstrap.go"], + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/bootstrap/api:go_default_library", + "//pkg/client/listers/core/internalversion:go_default_library", + "//vendor:github.com/golang/glog", + "//vendor:k8s.io/apimachinery/pkg/api/errors", + "//vendor:k8s.io/apiserver/pkg/authentication/user", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go b/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go new file mode 100644 index 00000000000..862393fb30c --- /dev/null +++ b/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go @@ -0,0 +1,168 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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 bootstrap provides a token authenticator for TLS bootstrap secrets. +*/ +package bootstrap + +import ( + "fmt" + "regexp" + "time" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/kubernetes/pkg/api" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" + "k8s.io/kubernetes/pkg/client/listers/core/internalversion" +) + +// TODO: A few methods in this package is copied from other sources. Either +// because the existing functionality isn't exported or because it is in a +// package that shouldn't be directly imported by this packages. + +// NewTokenAuthenticator initializes a bootstrap token authenticator. +// +// Lister is expected to be for the "kube-system" namespace. +func NewTokenAuthenticator(lister internalversion.SecretNamespaceLister) *TokenAuthenticator { + return &TokenAuthenticator{lister} +} + +// TokenAuthenticator authenticates bootstrap tokens from secrets in the API server. +type TokenAuthenticator struct { + lister internalversion.SecretNamespaceLister +} + +// AuthenticateToken tries to match the provided token to a bootstrap token secret +// in a given namespace. If found, it authenticates the token in the +// "system:bootstrappers" group and with the "system:bootstrap:(token-id)" username. +// +// All secrets must be of type "bootstrap.kubernetes.io/token". An example secret: +// +// apiVersion: v1 +// kind: Secret +// metadata: +// # Name MUST be of form "bootstrap-token-( token id )". +// name: bootstrap-token-( token id ) +// namespace: kube-system +// # Only secrets of this type will be evaluated. +// type: bootstrap.kubernetes.io/token +// data: +// token-secret: ( private part of token ) +// token-id: ( token id ) +// # Required key usage. +// usage-bootstrap-authentication: true +// # May also contain an expiry. +// +// Tokens are expected to be of the form: +// +// ( token-id ).( token-secret ) +// +func (t *TokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) { + tokenID, tokenSecret, err := parseToken(token) + if err != nil { + // Token isn't of the correct form, ignore it. + return nil, false, nil + } + + secretName := bootstrapapi.BootstrapTokenSecretPrefix + tokenID + secret, err := t.lister.Get(secretName) + if err != nil { + if errors.IsNotFound(err) { + return nil, false, nil + } + return nil, false, err + } + + if string(secret.Type) != string(bootstrapapi.SecretTypeBootstrapToken) || secret.Data == nil { + return nil, false, nil + } + + ts := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) + if ts != tokenSecret { + return nil, false, nil + } + + id := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) + if id != tokenID { + return nil, false, nil + } + + if isSecretExpired(secret) { + return nil, false, nil + } + + if getSecretString(secret, bootstrapapi.BootstrapTokenUsageAuthentication) != "true" { + glog.V(3).Infof("Bearer token matching bootstrap Secret %s/%s not marked %s=true.", + secret.Namespace, secret.Name, bootstrapapi.BootstrapTokenUsageAuthentication) + return nil, false, nil + } + + return &user.DefaultInfo{ + Name: bootstrapapi.BootstrapUserPrefix + string(id), + Groups: []string{bootstrapapi.BootstrapGroup}, + }, true, nil +} + +// Copied from k8s.io/kubernetes/pkg/bootstrap/api +func getSecretString(secret *api.Secret, key string) string { + if secret.Data == nil { + return "" + } + if val, ok := secret.Data[key]; ok { + return string(val) + } + return "" +} + +// Copied from k8s.io/kubernetes/pkg/bootstrap/api +func isSecretExpired(secret *api.Secret) bool { + expiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey) + if len(expiration) > 0 { + expTime, err2 := time.Parse(time.RFC3339, expiration) + if err2 != nil { + glog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.", + expiration, secret.Namespace, secret.Name, err2) + return true + } + if time.Now().After(expTime) { + glog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v", + secret.Namespace, secret.Name, expiration) + return true + } + } + return false +} + +// Copied from kubernetes/cmd/kubeadm/app/util/token + +var ( + tokenRegexpString = "^([a-z0-9]{6})\\.([a-z0-9]{16})$" + tokenRegexp = regexp.MustCompile(tokenRegexpString) +) + +// parseToken tries and parse a valid token from a string. +// A token ID and token secret are returned in case of success, an error otherwise. +func parseToken(s string) (string, string, error) { + split := tokenRegexp.FindStringSubmatch(s) + if len(split) != 3 { + return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, tokenRegexpString) + } + return split[1], split[2], nil +} diff --git a/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap_test.go b/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap_test.go new file mode 100644 index 00000000000..0061c92f1e9 --- /dev/null +++ b/plugin/pkg/auth/authenticator/token/bootstrap/bootstrap_test.go @@ -0,0 +1,228 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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 bootstrap + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/kubernetes/pkg/api" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" +) + +type lister struct { + secrets []*api.Secret +} + +func (l *lister) List(selector labels.Selector) (ret []*api.Secret, err error) { + return l.secrets, nil +} + +func (l *lister) Get(name string) (*api.Secret, error) { + for _, s := range l.secrets { + if s.Name == name { + return s, nil + } + } + return nil, errors.NewNotFound(schema.GroupResource{}, name) +} + +const ( + tokenID = "foobar" // 6 letters + tokenSecret = "circumnavigation" // 16 letters +) + +func TestTokenAuthenticator(t *testing.T) { + tests := []struct { + name string + + secrets []*api.Secret + token string + + wantNotFound bool + wantUser *user.DefaultInfo + }{ + { + name: "valid token", + secrets: []*api.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapapi.BootstrapTokenSecretPrefix + tokenID, + }, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(tokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), + bootstrapapi.BootstrapTokenUsageAuthentication: []byte("true"), + }, + Type: "bootstrap.kubernetes.io/token", + }, + }, + token: tokenID + "." + tokenSecret, + wantUser: &user.DefaultInfo{ + Name: "system:bootstrap:" + tokenID, + Groups: []string{"system:bootstrappers"}, + }, + }, + { + name: "invalid secret name", + secrets: []*api.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "bad-name", + }, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(tokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), + bootstrapapi.BootstrapTokenUsageAuthentication: []byte("true"), + }, + Type: "bootstrap.kubernetes.io/token", + }, + }, + token: tokenID + "." + tokenSecret, + wantNotFound: true, + }, + { + name: "no usage", + secrets: []*api.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapapi.BootstrapTokenSecretPrefix + tokenID, + }, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(tokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), + }, + Type: "bootstrap.kubernetes.io/token", + }, + }, + token: tokenID + "." + tokenSecret, + wantNotFound: true, + }, + { + name: "wrong token", + secrets: []*api.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapapi.BootstrapTokenSecretPrefix + tokenID, + }, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(tokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), + bootstrapapi.BootstrapTokenUsageAuthentication: []byte("true"), + }, + Type: "bootstrap.kubernetes.io/token", + }, + }, + token: "barfoo" + "." + tokenSecret, + wantNotFound: true, + }, + { + name: "expired token", + secrets: []*api.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapapi.BootstrapTokenSecretPrefix + tokenID, + }, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(tokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), + bootstrapapi.BootstrapTokenUsageAuthentication: []byte("true"), + bootstrapapi.BootstrapTokenExpirationKey: []byte("2009-11-10T23:00:00Z"), + }, + Type: "bootstrap.kubernetes.io/token", + }, + }, + token: tokenID + "." + tokenSecret, + wantNotFound: true, + }, + { + name: "not expired token", + secrets: []*api.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapapi.BootstrapTokenSecretPrefix + tokenID, + }, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(tokenID), + bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), + bootstrapapi.BootstrapTokenUsageAuthentication: []byte("true"), + bootstrapapi.BootstrapTokenExpirationKey: []byte("2109-11-10T23:00:00Z"), + }, + Type: "bootstrap.kubernetes.io/token", + }, + }, + token: tokenID + "." + tokenSecret, + wantUser: &user.DefaultInfo{ + Name: "system:bootstrap:" + tokenID, + Groups: []string{"system:bootstrappers"}, + }, + }, + { + name: "token id wrong length", + secrets: []*api.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapapi.BootstrapTokenSecretPrefix + "foo", + }, + Data: map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte("foo"), + bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret), + bootstrapapi.BootstrapTokenUsageAuthentication: []byte("true"), + }, + Type: "bootstrap.kubernetes.io/token", + }, + }, + // Token ID must be 6 characters. + token: "foo" + "." + tokenSecret, + wantNotFound: true, + }, + } + + for _, test := range tests { + func() { + a := NewTokenAuthenticator(&lister{test.secrets}) + u, found, err := a.AuthenticateToken(test.token) + if err != nil { + t.Errorf("test %q returned an error: %v", test.name, err) + return + } + + if !found { + if !test.wantNotFound { + t.Errorf("test %q expected to get user", test.name) + } + return + } + + if test.wantNotFound { + t.Errorf("test %q expected to not get a user", test.name) + return + } + + gotUser := u.(*user.DefaultInfo) + + if !reflect.DeepEqual(gotUser, test.wantUser) { + t.Errorf("test %q want user=%#v, got=%#v", test.name, test.wantUser, gotUser) + } + }() + } +}