diff --git a/cmd/kube-apiserver/app/BUILD b/cmd/kube-apiserver/app/BUILD index 16e8215898..876316b7aa 100644 --- a/cmd/kube-apiserver/app/BUILD +++ b/cmd/kube-apiserver/app/BUILD @@ -30,6 +30,7 @@ go_library( "//pkg/client/informers/informers_generated/internalversion:go_default_library", "//pkg/cloudprovider:go_default_library", "//pkg/controller/serviceaccount:go_default_library", + "//pkg/features:go_default_library", "//pkg/generated/openapi:go_default_library", "//pkg/kubeapiserver:go_default_library", "//pkg/kubeapiserver/admission:go_default_library", @@ -44,6 +45,7 @@ go_library( "//pkg/quota/install:go_default_library", "//pkg/registry/cachesize:go_default_library", "//pkg/registry/rbac/rest:go_default_library", + "//pkg/serviceaccount:go_default_library", "//pkg/util/flag:go_default_library", "//pkg/util/reflector/prometheus:go_default_library", "//pkg/util/workqueue/prometheus:go_default_library", @@ -75,10 +77,12 @@ go_library( "//vendor/k8s.io/apiserver/pkg/server/options/encryptionconfig:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/storage:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/etcd3/preflight:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/client-go/informers:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library", "//vendor/k8s.io/client-go/tools/cache:go_default_library", + "//vendor/k8s.io/client-go/util/cert:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1:go_default_library", diff --git a/cmd/kube-apiserver/app/options/options.go b/cmd/kube-apiserver/app/options/options.go index 1f57add7c3..59e2b96625 100644 --- a/cmd/kube-apiserver/app/options/options.go +++ b/cmd/kube-apiserver/app/options/options.go @@ -71,6 +71,8 @@ type ServerRunOptions struct { MasterCount int EndpointReconcilerType string + + ServiceAccountSigningKeyFile string } // NewServerRunOptions creates a new ServerRunOptions object with default parameters @@ -231,4 +233,7 @@ func (s *ServerRunOptions) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&s.EnableAggregatorRouting, "enable-aggregator-routing", s.EnableAggregatorRouting, "Turns on aggregator routing requests to endoints IP rather than cluster IP.") + fs.StringVar(&s.ServiceAccountSigningKeyFile, "service-account-signing-key-file", s.ServiceAccountSigningKeyFile, ""+ + "Path to the file that contains the current private key of the service account token issuer. The issuer will sign issued ID tokens with this private key. (Ignored unless alpha TokenRequest is enabled") + } diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 2fc7245a48..2825ecdf74 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -44,22 +44,23 @@ import ( utilwait "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/admission" webhookconfig "k8s.io/apiserver/pkg/admission/plugin/webhook/config" + webhookinit "k8s.io/apiserver/pkg/admission/plugin/webhook/initializer" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/filters" serveroptions "k8s.io/apiserver/pkg/server/options" "k8s.io/apiserver/pkg/server/options/encryptionconfig" serverstorage "k8s.io/apiserver/pkg/server/storage" - aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" - openapi "k8s.io/kube-openapi/pkg/common" - - webhookinit "k8s.io/apiserver/pkg/admission/plugin/webhook/initializer" - "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/storage/etcd3/preflight" + utilfeature "k8s.io/apiserver/pkg/util/feature" clientgoinformers "k8s.io/client-go/informers" clientgoclientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + certutil "k8s.io/client-go/util/cert" + aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" + openapi "k8s.io/kube-openapi/pkg/common" "k8s.io/kubernetes/cmd/kube-apiserver/app/options" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/admissionregistration" @@ -76,6 +77,7 @@ import ( informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" "k8s.io/kubernetes/pkg/cloudprovider" serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount" + "k8s.io/kubernetes/pkg/features" generatedopenapi "k8s.io/kubernetes/pkg/generated/openapi" "k8s.io/kubernetes/pkg/kubeapiserver" kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission" @@ -89,13 +91,14 @@ import ( quotainstall "k8s.io/kubernetes/pkg/quota/install" "k8s.io/kubernetes/pkg/registry/cachesize" rbacrest "k8s.io/kubernetes/pkg/registry/rbac/rest" + "k8s.io/kubernetes/pkg/serviceaccount" "k8s.io/kubernetes/pkg/version" + "k8s.io/kubernetes/pkg/version/verflag" "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/bootstrap" utilflag "k8s.io/kubernetes/pkg/util/flag" _ "k8s.io/kubernetes/pkg/util/reflector/prometheus" // for reflector metric registration _ "k8s.io/kubernetes/pkg/util/workqueue/prometheus" // for workqueue metric registration - "k8s.io/kubernetes/pkg/version/verflag" ) const etcdRetryLimit = 60 @@ -322,6 +325,21 @@ func CreateKubeAPIServerConfig(s *options.ServerRunOptions, nodeTunneler tunnele return nil, nil, nil, nil, nil, err } + var issuer serviceaccount.TokenGenerator + if s.ServiceAccountSigningKeyFile != "" || s.Authentication.ServiceAccounts.Issuer != "" { + if !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) { + return nil, nil, nil, nil, nil, fmt.Errorf("the TokenRequest feature is not enabled but --service-account-signing-key-file and/or --service-account-issuer-id flags were passed") + } + if s.ServiceAccountSigningKeyFile == "" || s.Authentication.ServiceAccounts.Issuer == "" { + return nil, nil, nil, nil, nil, fmt.Errorf("service-account-signing-key-file and service-account-issuer should be specified together") + } + sk, err := certutil.PrivateKeyFromFile(s.ServiceAccountSigningKeyFile) + if err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("failed to parse service-account-issuer-key-file: %v", err) + } + issuer = serviceaccount.JWTTokenGenerator(s.Authentication.ServiceAccounts.Issuer, sk) + } + config := &master.Config{ GenericConfig: genericConfig, ExtraConfig: master.ExtraConfig{ @@ -353,6 +371,7 @@ func CreateKubeAPIServerConfig(s *options.ServerRunOptions, nodeTunneler tunnele EndpointReconcilerType: reconcilers.Type(s.EndpointReconcilerType), MasterCount: s.MasterCount, + ServiceAccountIssuer: issuer, }, } diff --git a/pkg/controller/serviceaccount/tokengetter.go b/pkg/controller/serviceaccount/tokengetter.go index d21f578b29..b965ae9d3e 100644 --- a/pkg/controller/serviceaccount/tokengetter.go +++ b/pkg/controller/serviceaccount/tokengetter.go @@ -95,7 +95,7 @@ func NewGetterFromStorageInterface( saOpts := generic.RESTOptions{StorageConfig: saConfig, Decorator: generic.UndecoratedStorage, ResourcePrefix: saPrefix} secretOpts := generic.RESTOptions{StorageConfig: secretConfig, Decorator: generic.UndecoratedStorage, ResourcePrefix: secretPrefix} return NewGetterFromRegistries( - serviceaccountregistry.NewRegistry(serviceaccountstore.NewREST(saOpts)), + serviceaccountregistry.NewRegistry(serviceaccountstore.NewREST(saOpts, nil, nil, nil)), secret.NewRegistry(secretstore.NewREST(secretOpts)), ) } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index e704f958de..3c355bb45c 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -244,6 +244,12 @@ const ( // // Schedule DaemonSet Pods by default scheduler instead of DaemonSet controller NoDaemonSetScheduler utilfeature.Feature = "NoDaemonSetScheduler" + + // owner: @mikedanese + // alpha: v1.10 + // + // Implement TokenRequest endpoint on service account resources. + TokenRequest utilfeature.Feature = "TokenRequest" ) func init() { @@ -286,6 +292,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS SupportPodPidsLimit: {Default: false, PreRelease: utilfeature.Alpha}, HyperVContainer: {Default: false, PreRelease: utilfeature.Alpha}, NoDaemonSetScheduler: {Default: false, PreRelease: utilfeature.Alpha}, + TokenRequest: {Default: false, PreRelease: utilfeature.Alpha}, // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: diff --git a/pkg/kubeapiserver/options/authentication.go b/pkg/kubeapiserver/options/authentication.go index b1b27ba5ac..c24bbacac6 100644 --- a/pkg/kubeapiserver/options/authentication.go +++ b/pkg/kubeapiserver/options/authentication.go @@ -18,6 +18,7 @@ package options import ( "fmt" + "net/url" "strings" "time" @@ -71,6 +72,7 @@ type PasswordFileAuthenticationOptions struct { type ServiceAccountAuthenticationOptions struct { KeyFiles []string Lookup bool + Issuer string } type TokenFileAuthenticationOptions struct { @@ -157,6 +159,12 @@ func (s *BuiltInAuthenticationOptions) Validate() []error { allErrors = append(allErrors, fmt.Errorf("oidc-issuer-url and oidc-client-id should be specified together")) } + if s.ServiceAccounts != nil && len(s.ServiceAccounts.Issuer) > 0 && strings.Contains(s.ServiceAccounts.Issuer, ":") { + if _, err := url.Parse(s.ServiceAccounts.Issuer); err != nil { + allErrors = append(allErrors, fmt.Errorf("service-account-issuer contained a ':' but was not a valid URL: %v", err)) + } + } + return allErrors } @@ -233,6 +241,10 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&s.ServiceAccounts.Lookup, "service-account-lookup", s.ServiceAccounts.Lookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.") + + fs.StringVar(&s.ServiceAccounts.Issuer, "service-account-issuer", s.ServiceAccounts.Issuer, ""+ + "Identifier of the service account token issuer. The issuer will assert this identifier "+ + "in \"iss\" claim of issued tokens. This value is a string or URI.") } if s.TokenFile != nil { diff --git a/pkg/master/BUILD b/pkg/master/BUILD index bf2798d13d..e8ac47c70e 100644 --- a/pkg/master/BUILD +++ b/pkg/master/BUILD @@ -68,6 +68,7 @@ go_library( "//pkg/registry/settings/rest:go_default_library", "//pkg/registry/storage/rest:go_default_library", "//pkg/routes:go_default_library", + "//pkg/serviceaccount:go_default_library", "//pkg/util/async:go_default_library", "//pkg/util/node:go_default_library", "//vendor/github.com/golang/glog:go_default_library", diff --git a/pkg/master/master.go b/pkg/master/master.go index de693ee342..c071649697 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -65,6 +65,7 @@ import ( "k8s.io/kubernetes/pkg/registry/core/endpoint" endpointsstorage "k8s.io/kubernetes/pkg/registry/core/endpoint/storage" "k8s.io/kubernetes/pkg/routes" + "k8s.io/kubernetes/pkg/serviceaccount" nodeutil "k8s.io/kubernetes/pkg/util/node" "github.com/golang/glog" @@ -155,6 +156,8 @@ type ExtraConfig struct { // Selects which reconciler to use EndpointReconcilerType reconcilers.Type + + ServiceAccountIssuer serviceaccount.TokenGenerator } type Config struct { @@ -318,6 +321,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) ServiceIPRange: c.ExtraConfig.ServiceIPRange, ServiceNodePortRange: c.ExtraConfig.ServiceNodePortRange, LoopbackClientConfig: c.GenericConfig.LoopbackClientConfig, + ServiceAccountIssuer: c.ExtraConfig.ServiceAccountIssuer, } m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider) } diff --git a/pkg/registry/core/rest/BUILD b/pkg/registry/core/rest/BUILD index 64d48f40ef..5b81c65aaf 100644 --- a/pkg/registry/core/rest/BUILD +++ b/pkg/registry/core/rest/BUILD @@ -25,6 +25,7 @@ go_library( "//pkg/api/legacyscheme:go_default_library", "//pkg/apis/core:go_default_library", "//pkg/client/clientset_generated/internalclientset/typed/policy/internalversion:go_default_library", + "//pkg/features:go_default_library", "//pkg/kubelet/client:go_default_library", "//pkg/master/ports:go_default_library", "//pkg/registry/core/componentstatus:go_default_library", @@ -50,6 +51,7 @@ go_library( "//pkg/registry/core/service/portallocator:go_default_library", "//pkg/registry/core/service/storage:go_default_library", "//pkg/registry/core/serviceaccount/storage:go_default_library", + "//pkg/serviceaccount:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", @@ -58,6 +60,7 @@ go_library( "//vendor/k8s.io/apiserver/pkg/server:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/storage:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/etcd/util:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library", ], ) diff --git a/pkg/registry/core/rest/storage_core.go b/pkg/registry/core/rest/storage_core.go index 96e2219a29..bfb8e1a4c1 100644 --- a/pkg/registry/core/rest/storage_core.go +++ b/pkg/registry/core/rest/storage_core.go @@ -34,10 +34,12 @@ import ( genericapiserver "k8s.io/apiserver/pkg/server" serverstorage "k8s.io/apiserver/pkg/server/storage" etcdutil "k8s.io/apiserver/pkg/storage/etcd/util" + utilfeature "k8s.io/apiserver/pkg/util/feature" restclient "k8s.io/client-go/rest" "k8s.io/kubernetes/pkg/api/legacyscheme" api "k8s.io/kubernetes/pkg/apis/core" policyclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/policy/internalversion" + "k8s.io/kubernetes/pkg/features" kubeletclient "k8s.io/kubernetes/pkg/kubelet/client" "k8s.io/kubernetes/pkg/master/ports" "k8s.io/kubernetes/pkg/registry/core/componentstatus" @@ -63,6 +65,7 @@ import ( "k8s.io/kubernetes/pkg/registry/core/service/portallocator" servicestore "k8s.io/kubernetes/pkg/registry/core/service/storage" serviceaccountstore "k8s.io/kubernetes/pkg/registry/core/serviceaccount/storage" + "k8s.io/kubernetes/pkg/serviceaccount" ) // LegacyRESTStorageProvider provides information needed to build RESTStorage for core, but @@ -78,6 +81,8 @@ type LegacyRESTStorageProvider struct { ServiceIPRange net.IPNet ServiceNodePortRange utilnet.PortRange + ServiceAccountIssuer serviceaccount.TokenGenerator + LoopbackClientConfig *restclient.Config } @@ -115,7 +120,6 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotastore.NewREST(restOptionsGetter) secretStorage := secretstore.NewREST(restOptionsGetter) - serviceAccountStorage := serviceaccountstore.NewREST(restOptionsGetter) persistentVolumeStorage, persistentVolumeStatusStorage := pvstore.NewREST(restOptionsGetter) persistentVolumeClaimStorage, persistentVolumeClaimStatusStorage := pvcstore.NewREST(restOptionsGetter) configMapStorage := configmapstore.NewREST(restOptionsGetter) @@ -137,6 +141,13 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi podDisruptionClient, ) + var serviceAccountStorage *serviceaccountstore.REST + if c.ServiceAccountIssuer != nil && utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) { + serviceAccountStorage = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, podStorage.Pod.Store, secretStorage.Store) + } else { + serviceAccountStorage = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, nil) + } + serviceRESTStorage, serviceStatusStorage := servicestore.NewREST(restOptionsGetter) serviceRegistry := service.NewRegistry(serviceRESTStorage) @@ -224,6 +235,9 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi if legacyscheme.Registry.IsEnabledVersion(schema.GroupVersion{Group: "policy", Version: "v1beta1"}) { restStorageMap["pods/eviction"] = podStorage.Eviction } + if serviceAccountStorage.Token != nil { + restStorageMap["serviceaccounts/token"] = serviceAccountStorage.Token + } apiGroupInfo.VersionedResourcesStorageMap["v1"] = restStorageMap return restStorage, apiGroupInfo, nil diff --git a/pkg/registry/core/serviceaccount/storage/BUILD b/pkg/registry/core/serviceaccount/storage/BUILD index 7718c30f1e..7fe652f2d9 100644 --- a/pkg/registry/core/serviceaccount/storage/BUILD +++ b/pkg/registry/core/serviceaccount/storage/BUILD @@ -25,12 +25,23 @@ go_test( go_library( name = "go_default_library", - srcs = ["storage.go"], + srcs = [ + "storage.go", + "token.go", + ], importpath = "k8s.io/kubernetes/pkg/registry/core/serviceaccount/storage", deps = [ + "//pkg/apis/authentication:go_default_library", "//pkg/apis/core:go_default_library", "//pkg/registry/core/serviceaccount:go_default_library", + "//pkg/serviceaccount:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", + "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", diff --git a/pkg/registry/core/serviceaccount/storage/storage.go b/pkg/registry/core/serviceaccount/storage/storage.go index 1b889374ad..ae47cae068 100644 --- a/pkg/registry/core/serviceaccount/storage/storage.go +++ b/pkg/registry/core/serviceaccount/storage/storage.go @@ -23,14 +23,16 @@ import ( "k8s.io/apiserver/pkg/registry/rest" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/registry/core/serviceaccount" + token "k8s.io/kubernetes/pkg/serviceaccount" ) type REST struct { *genericregistry.Store + Token *TokenREST } // NewREST returns a RESTStorage object that will work against service accounts. -func NewREST(optsGetter generic.RESTOptionsGetter) *REST { +func NewREST(optsGetter generic.RESTOptionsGetter, issuer token.TokenGenerator, podStorage, secretStorage *genericregistry.Store) *REST { store := &genericregistry.Store{ NewFunc: func() runtime.Object { return &api.ServiceAccount{} }, NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} }, @@ -45,7 +47,21 @@ func NewREST(optsGetter generic.RESTOptionsGetter) *REST { if err := store.CompleteWithOptions(options); err != nil { panic(err) // TODO: Propagate error up } - return &REST{store} + + var trest *TokenREST + if issuer != nil && podStorage != nil && secretStorage != nil { + trest = &TokenREST{ + svcaccts: store, + issuer: issuer, + pods: podStorage, + secrets: secretStorage, + } + } + + return &REST{ + Store: store, + Token: trest, + } } // Implement ShortNamesProvider diff --git a/pkg/registry/core/serviceaccount/storage/storage_test.go b/pkg/registry/core/serviceaccount/storage/storage_test.go index ef42b7191a..1a41a9efcb 100644 --- a/pkg/registry/core/serviceaccount/storage/storage_test.go +++ b/pkg/registry/core/serviceaccount/storage/storage_test.go @@ -38,7 +38,7 @@ func newStorage(t *testing.T) (*REST, *etcdtesting.EtcdTestServer) { DeleteCollectionWorkers: 1, ResourcePrefix: "serviceaccounts", } - return NewREST(restOptions), server + return NewREST(restOptions, nil, nil, nil), server } func validNewServiceAccount(name string) *api.ServiceAccount { diff --git a/pkg/registry/core/serviceaccount/storage/token.go b/pkg/registry/core/serviceaccount/storage/token.go new file mode 100644 index 0000000000..63bffdfdc8 --- /dev/null +++ b/pkg/registry/core/serviceaccount/storage/token.go @@ -0,0 +1,115 @@ +/* +Copyright 2018 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 storage + +import ( + "fmt" + + authenticationapiv1 "k8s.io/api/authentication/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + authenticationapi "k8s.io/kubernetes/pkg/apis/authentication" + api "k8s.io/kubernetes/pkg/apis/core" + token "k8s.io/kubernetes/pkg/serviceaccount" +) + +func (r *TokenREST) New() runtime.Object { + return &authenticationapi.TokenRequest{} +} + +type TokenREST struct { + svcaccts getter + pods getter + secrets getter + issuer token.TokenGenerator +} + +var _ = rest.NamedCreater(&TokenREST{}) + +func (r *TokenREST) Create(ctx genericapirequest.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, includeUninitialized bool) (runtime.Object, error) { + if err := createValidation(obj); err != nil { + return nil, err + } + + out := obj.(*authenticationapi.TokenRequest) + + svcacctObj, err := r.svcaccts.Get(ctx, name, &metav1.GetOptions{}) + if err != nil { + return nil, err + } + svcacct := svcacctObj.(*api.ServiceAccount) + + var ( + pod *api.Pod + secret *api.Secret + ) + + if ref := out.Spec.BoundObjectRef; ref != nil { + var uid types.UID + + gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind) + switch { + case gvk.Group == "" && gvk.Kind == "Pod": + podObj, err := r.pods.Get(ctx, ref.Name, &metav1.GetOptions{}) + if err != nil { + return nil, err + } + pod = podObj.(*api.Pod) + uid = pod.UID + case gvk.Group == "" && gvk.Kind == "Secret": + secretObj, err := r.secrets.Get(ctx, ref.Name, &metav1.GetOptions{}) + if err != nil { + return nil, err + } + secret = secretObj.(*api.Secret) + uid = secret.UID + default: + return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token to object of type %s", gvk.String())) + } + if ref.UID != "" && uid != ref.UID { + return nil, errors.NewConflict(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, ref.Name, fmt.Errorf("the UID in the bound object reference (%s) does not match the UID in record (%s). The object might have been deleted and then recreated", ref.UID, uid)) + } + } + sc, pc := token.Claims(*svcacct, pod, secret, out.Spec.ExpirationSeconds, out.Spec.Audiences) + tokdata, err := r.issuer.GenerateToken(sc, pc) + if err != nil { + return nil, fmt.Errorf("failed to generate token: %v", err) + } + + out.Status = authenticationapi.TokenRequestStatus{ + Token: tokdata, + ExpirationTimestamp: metav1.Time{Time: sc.Expiry.Time()}, + } + return out, nil +} + +func (r *TokenREST) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: authenticationapiv1.SchemeGroupVersion.Group, + Version: authenticationapiv1.SchemeGroupVersion.Version, + Kind: "TokenRequest", + } +} + +type getter interface { + Get(ctx genericapirequest.Context, name string, options *metav1.GetOptions) (runtime.Object, error) +} diff --git a/pkg/serviceaccount/BUILD b/pkg/serviceaccount/BUILD index 96a7ef5bb6..eaa64787b1 100644 --- a/pkg/serviceaccount/BUILD +++ b/pkg/serviceaccount/BUILD @@ -9,6 +9,7 @@ load( go_library( name = "go_default_library", srcs = [ + "claims.go", "jwt.go", "legacy.go", "util.go", @@ -57,9 +58,14 @@ filegroup( go_test( name = "go_default_test", - srcs = ["util_test.go"], + srcs = [ + "claims_test.go", + "util_test.go", + ], embed = [":go_default_library"], deps = [ + "//pkg/apis/core:go_default_library", + "//vendor/gopkg.in/square/go-jose.v2/jwt:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", ], diff --git a/pkg/serviceaccount/claims.go b/pkg/serviceaccount/claims.go new file mode 100644 index 0000000000..084af2dfaa --- /dev/null +++ b/pkg/serviceaccount/claims.go @@ -0,0 +1,78 @@ +/* +Copyright 2018 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 serviceaccount + +import ( + "time" + + apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/kubernetes/pkg/apis/core" + + "gopkg.in/square/go-jose.v2/jwt" +) + +// time.Now stubbed out to allow testing +var now = time.Now + +type privateClaims struct { + Kubernetes kubernetes `json:"kubernetes.io,omitempty"` +} + +type kubernetes struct { + Namespace string `json:"namespace,omitempty"` + Svcacct ref `json:"serviceaccount,omitempty"` + Pod *ref `json:"pod,omitempty"` + Secret *ref `json:"secret,omitempty"` +} + +type ref struct { + Name string `json:"name,omitempty"` + UID string `json:"uid,omitempty"` +} + +func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, expirationSeconds int64, audience []string) (*jwt.Claims, interface{}) { + now := now() + sc := &jwt.Claims{ + Subject: apiserverserviceaccount.MakeUsername(sa.Namespace, sa.Name), + Audience: jwt.Audience(audience), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Duration(expirationSeconds) * time.Second)), + } + pc := &privateClaims{ + Kubernetes: kubernetes{ + Namespace: sa.Namespace, + Svcacct: ref{ + Name: sa.Name, + UID: string(sa.UID), + }, + }, + } + switch { + case pod != nil: + pc.Kubernetes.Pod = &ref{ + Name: pod.Name, + UID: string(pod.UID), + } + case secret != nil: + pc.Kubernetes.Secret = &ref{ + Name: secret.Name, + UID: string(secret.UID), + } + } + return sc, pc +} diff --git a/pkg/serviceaccount/claims_test.go b/pkg/serviceaccount/claims_test.go new file mode 100644 index 0000000000..aec036f8d6 --- /dev/null +++ b/pkg/serviceaccount/claims_test.go @@ -0,0 +1,184 @@ +/* +Copyright 2018 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 serviceaccount + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/apis/core" + + "gopkg.in/square/go-jose.v2/jwt" +) + +func init() { + now = func() time.Time { + // epoch time: 1514764800 + return time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC) + } +} + +func TestClaims(t *testing.T) { + sa := core.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "myns", + Name: "mysvcacct", + UID: "mysvcacct-uid", + }, + } + pod := &core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "myns", + Name: "mypod", + UID: "mypod-uid", + }, + } + sec := &core.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "myns", + Name: "mysecret", + UID: "mysecret-uid", + }, + } + cs := []struct { + // input + sa core.ServiceAccount + pod *core.Pod + sec *core.Secret + exp int64 + aud []string + // desired + sc *jwt.Claims + pc *privateClaims + }{ + { + // pod and secret + sa: sa, + pod: pod, + sec: sec, + // really fast + exp: 0, + // nil audience + aud: nil, + + sc: &jwt.Claims{ + Subject: "system:serviceaccount:myns:mysvcacct", + IssuedAt: jwt.NumericDate(1514764800), + NotBefore: jwt.NumericDate(1514764800), + Expiry: jwt.NumericDate(1514764800), + }, + pc: &privateClaims{ + Kubernetes: kubernetes{ + Namespace: "myns", + Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"}, + Pod: &ref{Name: "mypod", UID: "mypod-uid"}, + }, + }, + }, + { + // pod + sa: sa, + pod: pod, + // empty audience + aud: []string{}, + exp: 100, + + sc: &jwt.Claims{ + Subject: "system:serviceaccount:myns:mysvcacct", + IssuedAt: jwt.NumericDate(1514764800), + NotBefore: jwt.NumericDate(1514764800), + Expiry: jwt.NumericDate(1514764800 + 100), + }, + pc: &privateClaims{ + Kubernetes: kubernetes{ + Namespace: "myns", + Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"}, + Pod: &ref{Name: "mypod", UID: "mypod-uid"}, + }, + }, + }, + { + // secret + sa: sa, + sec: sec, + exp: 100, + // single member audience + aud: []string{"1"}, + + sc: &jwt.Claims{ + Subject: "system:serviceaccount:myns:mysvcacct", + Audience: []string{"1"}, + IssuedAt: jwt.NumericDate(1514764800), + NotBefore: jwt.NumericDate(1514764800), + Expiry: jwt.NumericDate(1514764800 + 100), + }, + pc: &privateClaims{ + Kubernetes: kubernetes{ + Namespace: "myns", + Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"}, + Secret: &ref{Name: "mysecret", UID: "mysecret-uid"}, + }, + }, + }, + { + // no obj binding + sa: sa, + exp: 100, + // multimember audience + aud: []string{"1", "2"}, + + sc: &jwt.Claims{ + Subject: "system:serviceaccount:myns:mysvcacct", + Audience: []string{"1", "2"}, + IssuedAt: jwt.NumericDate(1514764800), + NotBefore: jwt.NumericDate(1514764800), + Expiry: jwt.NumericDate(1514764800 + 100), + }, + pc: &privateClaims{ + Kubernetes: kubernetes{ + Namespace: "myns", + Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"}, + }, + }, + }, + } + for i, c := range cs { + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + // comparing json spews has the benefit over + // reflect.DeepEqual that we are also asserting that + // claims structs are json serializable + spew := func(obj interface{}) string { + b, err := json.Marshal(obj) + if err != nil { + t.Fatalf("err, couldn't marshal claims: %v", err) + } + return string(b) + } + + sc, pc := Claims(c.sa, c.pod, c.sec, c.exp, c.aud) + if spew(sc) != spew(c.sc) { + t.Errorf("standard claims differed\n\tsaw:\t%s\n\twant:\t%s", spew(sc), spew(c.sc)) + } + if spew(pc) != spew(c.pc) { + t.Errorf("private claims differed\n\tsaw: %s\n\twant: %s", spew(pc), spew(c.pc)) + } + }) + } +} diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/BUILD b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/BUILD index dd86cfecd2..4c9f03aa4d 100644 --- a/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/BUILD +++ b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/BUILD @@ -32,9 +32,11 @@ go_library( "service.go", "service_expansion.go", "serviceaccount.go", + "serviceaccount_expansion.go", ], importpath = "k8s.io/client-go/kubernetes/typed/core/v1", deps = [ + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/extensions/v1beta1:go_default_library", "//vendor/k8s.io/api/policy/v1beta1:go_default_library", diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/fake/BUILD b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/fake/BUILD index f62201d87d..7403a4997d 100644 --- a/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/fake/BUILD +++ b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/fake/BUILD @@ -31,9 +31,11 @@ go_library( "fake_service.go", "fake_service_expansion.go", "fake_serviceaccount.go", + "fake_serviceaccount_expansion.go", ], importpath = "k8s.io/client-go/kubernetes/typed/core/v1/fake", deps = [ + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/extensions/v1beta1:go_default_library", "//vendor/k8s.io/api/policy/v1beta1:go_default_library", diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/fake/fake_serviceaccount_expansion.go b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/fake/fake_serviceaccount_expansion.go new file mode 100644 index 0000000000..a0efbcc2fe --- /dev/null +++ b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/fake/fake_serviceaccount_expansion.go @@ -0,0 +1,31 @@ +/* +Copyright 2018 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 fake + +import ( + authenticationv1 "k8s.io/api/authentication/v1" + core "k8s.io/client-go/testing" +) + +func (c *FakeServiceAccounts) CreateToken(name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + obj, err := c.Fake.Invokes(core.NewCreateSubresourceAction(serviceaccountsResource, name, "token", c.ns, tr), &authenticationv1.TokenRequest{}) + + if obj == nil { + return nil, err + } + return obj.(*authenticationv1.TokenRequest), err +} diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/generated_expansion.go b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/generated_expansion.go index 3f4b5f89c7..5bb5f4cd60 100644 --- a/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/generated_expansion.go +++ b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/generated_expansion.go @@ -35,5 +35,3 @@ type ReplicationControllerExpansion interface{} type ResourceQuotaExpansion interface{} type SecretExpansion interface{} - -type ServiceAccountExpansion interface{} diff --git a/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/serviceaccount_expansion.go b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/serviceaccount_expansion.go new file mode 100644 index 0000000000..eaf643f154 --- /dev/null +++ b/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/serviceaccount_expansion.go @@ -0,0 +1,41 @@ +/* +Copyright 2018 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 v1 + +import ( + authenticationv1 "k8s.io/api/authentication/v1" +) + +// The ServiceAccountExpansion interface allows manually adding extra methods +// to the ServiceAccountInterface. +type ServiceAccountExpansion interface { + CreateToken(name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) +} + +// CreateToken creates a new token for a serviceaccount. +func (c *serviceAccounts) CreateToken(name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + result := &authenticationv1.TokenRequest{} + err := c.client.Post(). + Namespace(c.ns). + Resource("serviceaccounts"). + SubResource("token"). + Name(name). + Body(tr). + Do(). + Into(result) + return result, err +} diff --git a/test/integration/auth/BUILD b/test/integration/auth/BUILD index 6da566db65..23bf4fef6c 100644 --- a/test/integration/auth/BUILD +++ b/test/integration/auth/BUILD @@ -15,6 +15,7 @@ go_test( "main_test.go", "node_test.go", "rbac_test.go", + "svcaccttoken_test.go", ], tags = ["integration"], deps = [ @@ -41,6 +42,7 @@ go_test( "//pkg/registry/rbac/role/storage:go_default_library", "//pkg/registry/rbac/rolebinding:go_default_library", "//pkg/registry/rbac/rolebinding/storage:go_default_library", + "//pkg/serviceaccount:go_default_library", "//plugin/pkg/admission/admit:go_default_library", "//plugin/pkg/admission/noderestriction:go_default_library", "//plugin/pkg/auth/authenticator/token/bootstrap:go_default_library", @@ -49,7 +51,9 @@ go_test( "//test/integration:go_default_library", "//test/integration/framework:go_default_library", "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/authentication/v1beta1:go_default_library", + "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/storage/v1beta1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", @@ -78,6 +82,7 @@ go_test( "//vendor/k8s.io/client-go/tools/bootstrap/token/api:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library", "//vendor/k8s.io/client-go/transport:go_default_library", + "//vendor/k8s.io/client-go/util/cert:go_default_library", ], ) diff --git a/test/integration/auth/svcaccttoken_test.go b/test/integration/auth/svcaccttoken_test.go new file mode 100644 index 0000000000..ac9fe62bd4 --- /dev/null +++ b/test/integration/auth/svcaccttoken_test.go @@ -0,0 +1,201 @@ +/* +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 auth + +import ( + "encoding/base64" + "encoding/json" + "strings" + "testing" + + authenticationv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authorization/authorizerfactory" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" + clientset "k8s.io/client-go/kubernetes" + certutil "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/pkg/features" + "k8s.io/kubernetes/pkg/serviceaccount" + "k8s.io/kubernetes/test/integration/framework" +) + +const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49 +AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0 +/IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== +-----END EC PRIVATE KEY-----` + +func TestServiceAccountTokenCreate(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TokenRequest, true)() + + // Build client config, clientset, and informers + sk, err := certutil.ParsePrivateKeyPEM([]byte(ecdsaPrivateKey)) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Start the server + masterConfig := framework.NewIntegrationTestMasterConfig() + masterConfig.GenericConfig.Authorization.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer() + masterConfig.ExtraConfig.ServiceAccountIssuer = serviceaccount.JWTTokenGenerator("https://foo.bar.example.com", sk) + + master, _, closeFn := framework.RunAMaster(masterConfig) + defer closeFn() + + cs, err := clientset.NewForConfig(master.GenericAPIServer.LoopbackClientConfig) + if err != nil { + t.Fatalf("err: %v", err) + } + + sa := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + + tr1 := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"aud"}, + }, + } + + _, err = cs.CoreV1().ServiceAccounts(sa.Namespace).Create(sa) + if err != nil { + t.Fatalf("err: %v", err) + } + tr1, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, tr1) + if err != nil { + t.Fatalf("err: %v", err) + } + + checkPayload(t, tr1.Status.Token, `"system:serviceaccount:default:test"`, "sub") + checkPayload(t, tr1.Status.Token, `["aud"]`, "aud") + checkPayload(t, tr1.Status.Token, "null", "kubernetes.io", "pod") + checkPayload(t, tr1.Status.Token, "null", "kubernetes.io", "secret") + checkPayload(t, tr1.Status.Token, `"default"`, "kubernetes.io", "namespace") + checkPayload(t, tr1.Status.Token, `"test"`, "kubernetes.io", "serviceaccount", "name") + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: sa.Namespace, + }, + Spec: v1.PodSpec{ + ServiceAccountName: sa.Name, + Containers: []v1.Container{{Name: "test-container", Image: "nginx"}}, + }, + } + _, err = cs.CoreV1().Pods(pod.Namespace).Create(pod) + if err != nil { + t.Fatalf("err: %v", err) + } + + tr2 := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"aud"}, + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "Pod", + APIVersion: "v1", + Name: pod.Name, + }, + }, + } + tr2, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, tr2) + if err != nil { + t.Fatalf("err: %v", err) + } + + checkPayload(t, tr2.Status.Token, `"system:serviceaccount:default:test"`, "sub") + checkPayload(t, tr2.Status.Token, `["aud"]`, "aud") + checkPayload(t, tr2.Status.Token, `"test-pod"`, "kubernetes.io", "pod", "name") + checkPayload(t, tr2.Status.Token, "null", "kubernetes.io", "secret") + checkPayload(t, tr2.Status.Token, `"default"`, "kubernetes.io", "namespace") + checkPayload(t, tr2.Status.Token, `"test"`, "kubernetes.io", "serviceaccount", "name") + + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: sa.Namespace, + }, + } + _, err = cs.CoreV1().Secrets(secret.Namespace).Create(secret) + if err != nil { + t.Fatalf("err: %v", err) + } + tr3 := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{"aud"}, + BoundObjectRef: &authenticationv1.BoundObjectReference{ + Kind: "Secret", + APIVersion: "v1", + Name: secret.Name, + }, + }, + } + tr3, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, tr3) + if err != nil { + t.Fatalf("err: %v", err) + } + + checkPayload(t, tr2.Status.Token, `"system:serviceaccount:default:test"`, "sub") + checkPayload(t, tr2.Status.Token, `["aud"]`, "aud") + checkPayload(t, tr2.Status.Token, `"test-pod"`, "kubernetes.io", "pod", "name") + checkPayload(t, tr2.Status.Token, `null`, "kubernetes.io", "secret") + checkPayload(t, tr2.Status.Token, `"default"`, "kubernetes.io", "namespace") + checkPayload(t, tr2.Status.Token, `"test"`, "kubernetes.io", "serviceaccount", "name") +} + +func checkPayload(t *testing.T, tok string, want string, parts ...string) { + t.Helper() + got := getSubObject(t, getPayload(t, tok), parts...) + if got != want { + t.Errorf("unexpected payload.\nsaw:\t%v\nwant:\t%v", got, want) + } +} + +func getSubObject(t *testing.T, b string, parts ...string) string { + t.Helper() + var obj interface{} + obj = make(map[string]interface{}) + if err := json.Unmarshal([]byte(b), &obj); err != nil { + t.Fatalf("err: %v", err) + } + for _, part := range parts { + obj = obj.(map[string]interface{})[part] + } + out, err := json.Marshal(obj) + if err != nil { + t.Fatalf("err: %v", err) + } + return string(out) +} + +func getPayload(t *testing.T, b string) string { + t.Helper() + parts := strings.Split(b, ".") + if len(parts) != 3 { + t.Fatalf("token did not have three parts: %v", b) + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + t.Fatalf("failed to base64 decode token: %v", err) + } + return string(payload) +}