mirror of https://github.com/k3s-io/k3s
add support for /token subresource in serviceaccount registry
parent
2b530438f1
commit
8ad1c6655b
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -35,5 +35,3 @@ type ReplicationControllerExpansion interface{}
|
|||
type ResourceQuotaExpansion interface{}
|
||||
|
||||
type SecretExpansion interface{}
|
||||
|
||||
type ServiceAccountExpansion interface{}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue