add support for /token subresource in serviceaccount registry

pull/6/head
Mike Danese 2018-02-05 20:53:25 -08:00
parent 2b530438f1
commit 8ad1c6655b
24 changed files with 774 additions and 15 deletions

View File

@ -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",

View File

@ -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")
}

View File

@ -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,
},
}

View File

@ -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)),
)
}

View File

@ -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:

View File

@ -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 {

View File

@ -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",

View File

@ -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)
}

View File

@ -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",
],
)

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -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",
],

View File

@ -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
}

View File

@ -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))
}
})
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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
}

View File

@ -35,5 +35,3 @@ type ReplicationControllerExpansion interface{}
type ResourceQuotaExpansion interface{}
type SecretExpansion interface{}
type ServiceAccountExpansion interface{}

View File

@ -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
}

View File

@ -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",
],
)

View File

@ -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)
}