2018-02-06 04:53:25 +00:00
|
|
|
/*
|
|
|
|
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 (
|
2018-02-25 04:14:59 +00:00
|
|
|
"crypto/ecdsa"
|
2018-02-06 04:53:25 +00:00
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2018-05-10 06:29:43 +00:00
|
|
|
"fmt"
|
2018-03-28 17:49:15 +00:00
|
|
|
"reflect"
|
2018-02-06 04:53:25 +00:00
|
|
|
"strings"
|
|
|
|
"testing"
|
2018-02-25 04:14:59 +00:00
|
|
|
"time"
|
2018-02-06 04:53:25 +00:00
|
|
|
|
2018-05-17 19:47:27 +00:00
|
|
|
"gopkg.in/square/go-jose.v2/jwt"
|
2018-10-23 01:01:40 +00:00
|
|
|
|
2018-02-06 04:53:25 +00:00
|
|
|
authenticationv1 "k8s.io/api/authentication/v1"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2018-02-23 19:40:43 +00:00
|
|
|
"k8s.io/apimachinery/pkg/types"
|
2018-10-24 02:44:59 +00:00
|
|
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
2018-02-25 04:14:59 +00:00
|
|
|
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
2018-05-17 19:47:27 +00:00
|
|
|
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
|
2018-02-06 04:53:25 +00:00
|
|
|
"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"
|
2018-12-06 19:00:53 +00:00
|
|
|
v1listers "k8s.io/client-go/listers/core/v1"
|
|
|
|
"k8s.io/client-go/tools/cache"
|
2018-02-06 04:53:25 +00:00
|
|
|
certutil "k8s.io/client-go/util/cert"
|
2018-05-17 19:47:27 +00:00
|
|
|
"k8s.io/kubernetes/pkg/apis/core"
|
2018-02-25 04:14:59 +00:00
|
|
|
serviceaccountgetter "k8s.io/kubernetes/pkg/controller/serviceaccount"
|
2018-02-06 04:53:25 +00:00
|
|
|
"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)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
pk := sk.(*ecdsa.PrivateKey).PublicKey
|
|
|
|
|
|
|
|
const iss = "https://foo.bar.example.com"
|
2018-10-24 02:44:59 +00:00
|
|
|
aud := authenticator.Audiences{"api"}
|
2018-02-25 04:14:59 +00:00
|
|
|
|
2018-05-10 06:29:43 +00:00
|
|
|
maxExpirationSeconds := int64(60 * 60)
|
|
|
|
maxExpirationDuration, err := time.ParseDuration(fmt.Sprintf("%ds", maxExpirationSeconds))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
2018-02-25 04:14:59 +00:00
|
|
|
gcs := &clientset.Clientset{}
|
2018-02-06 04:53:25 +00:00
|
|
|
|
|
|
|
// Start the server
|
|
|
|
masterConfig := framework.NewIntegrationTestMasterConfig()
|
|
|
|
masterConfig.GenericConfig.Authorization.Authorizer = authorizerfactory.NewAlwaysAllowAuthorizer()
|
2018-10-24 02:44:59 +00:00
|
|
|
masterConfig.GenericConfig.Authentication.APIAudiences = aud
|
2018-02-25 04:14:59 +00:00
|
|
|
masterConfig.GenericConfig.Authentication.Authenticator = bearertoken.New(
|
|
|
|
serviceaccount.JWTTokenAuthenticator(
|
|
|
|
iss,
|
|
|
|
[]interface{}{&pk},
|
2018-10-24 02:44:59 +00:00
|
|
|
aud,
|
2018-12-06 19:00:53 +00:00
|
|
|
serviceaccount.NewValidator(serviceaccountgetter.NewGetterFromClient(
|
|
|
|
gcs,
|
|
|
|
v1listers.NewSecretLister(newIndexer(func(namespace, name string) (interface{}, error) {
|
|
|
|
return gcs.CoreV1().Secrets(namespace).Get(name, metav1.GetOptions{})
|
|
|
|
})),
|
|
|
|
v1listers.NewServiceAccountLister(newIndexer(func(namespace, name string) (interface{}, error) {
|
|
|
|
return gcs.CoreV1().ServiceAccounts(namespace).Get(name, metav1.GetOptions{})
|
|
|
|
})),
|
|
|
|
v1listers.NewPodLister(newIndexer(func(namespace, name string) (interface{}, error) {
|
|
|
|
return gcs.CoreV1().Pods(namespace).Get(name, metav1.GetOptions{})
|
|
|
|
})),
|
|
|
|
)),
|
2018-02-25 04:14:59 +00:00
|
|
|
),
|
|
|
|
)
|
2018-04-11 18:24:06 +00:00
|
|
|
tokenGenerator, err := serviceaccount.JWTTokenGenerator(iss, sk)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
masterConfig.ExtraConfig.ServiceAccountIssuer = tokenGenerator
|
2018-05-10 06:29:43 +00:00
|
|
|
masterConfig.ExtraConfig.ServiceAccountMaxExpiration = maxExpirationDuration
|
2018-11-13 00:27:37 +00:00
|
|
|
masterConfig.GenericConfig.Authentication.APIAudiences = aud
|
2018-02-06 04:53:25 +00:00
|
|
|
|
|
|
|
master, _, closeFn := framework.RunAMaster(masterConfig)
|
|
|
|
defer closeFn()
|
|
|
|
|
|
|
|
cs, err := clientset.NewForConfig(master.GenericAPIServer.LoopbackClientConfig)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
*gcs = *cs
|
2018-02-06 04:53:25 +00:00
|
|
|
|
2018-02-23 19:40:43 +00:00
|
|
|
var (
|
|
|
|
sa = &v1.ServiceAccount{
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Name: "test-svcacct",
|
|
|
|
Namespace: "myns",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
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"}},
|
|
|
|
},
|
|
|
|
}
|
2018-02-23 22:47:54 +00:00
|
|
|
otherpod = &v1.Pod{
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Name: "other-test-pod",
|
|
|
|
Namespace: sa.Namespace,
|
|
|
|
},
|
|
|
|
Spec: v1.PodSpec{
|
|
|
|
ServiceAccountName: "other-" + sa.Name,
|
|
|
|
Containers: []v1.Container{{Name: "test-container", Image: "nginx"}},
|
|
|
|
},
|
|
|
|
}
|
2018-02-23 19:40:43 +00:00
|
|
|
secret = &v1.Secret{
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Name: "test-secret",
|
|
|
|
Namespace: sa.Namespace,
|
|
|
|
},
|
|
|
|
}
|
2018-02-06 04:53:25 +00:00
|
|
|
|
2018-02-23 19:40:43 +00:00
|
|
|
wrongUID = types.UID("wrong")
|
|
|
|
noUID = types.UID("")
|
|
|
|
)
|
2018-02-06 04:53:25 +00:00
|
|
|
|
2018-02-23 19:40:43 +00:00
|
|
|
t.Run("bound to service account", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
2018-05-17 19:47:27 +00:00
|
|
|
Audiences: []string{"api"},
|
2018-02-23 19:40:43 +00:00
|
|
|
},
|
|
|
|
}
|
2018-02-06 04:53:25 +00:00
|
|
|
|
2018-02-23 19:40:43 +00:00
|
|
|
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err == nil {
|
|
|
|
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
sa, delSvcAcct := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer delSvcAcct()
|
2018-02-23 19:40:43 +00:00
|
|
|
|
|
|
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
|
|
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
|
|
|
checkPayload(t, treq.Status.Token, "null", "kubernetes.io", "pod")
|
|
|
|
checkPayload(t, treq.Status.Token, "null", "kubernetes.io", "secret")
|
|
|
|
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
2018-02-25 04:14:59 +00:00
|
|
|
|
2018-03-28 17:49:15 +00:00
|
|
|
info := doTokenReview(t, cs, treq, false)
|
|
|
|
if info.Extra != nil {
|
|
|
|
t.Fatalf("expected Extra to be nil but got: %#v", info.Extra)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
delSvcAcct()
|
|
|
|
doTokenReview(t, cs, treq, true)
|
2018-02-23 19:40:43 +00:00
|
|
|
})
|
2018-02-06 04:53:25 +00:00
|
|
|
|
2018-02-23 19:40:43 +00:00
|
|
|
t.Run("bound to service account and pod", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
2018-05-17 19:47:27 +00:00
|
|
|
Audiences: []string{"api"},
|
2018-02-23 19:40:43 +00:00
|
|
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
|
|
|
Kind: "Pod",
|
|
|
|
APIVersion: "v1",
|
|
|
|
Name: pod.Name,
|
|
|
|
},
|
2018-02-06 04:53:25 +00:00
|
|
|
},
|
2018-02-23 19:40:43 +00:00
|
|
|
}
|
2018-02-06 04:53:25 +00:00
|
|
|
|
2018-02-23 19:40:43 +00:00
|
|
|
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err == nil {
|
|
|
|
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
|
|
|
|
}
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err == nil {
|
|
|
|
t.Fatalf("expected err creating token bound to nonexistant pod but got: %#v", resp)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
pod, delPod := createDeletePod(t, cs, pod)
|
|
|
|
defer delPod()
|
2018-02-23 19:40:43 +00:00
|
|
|
|
|
|
|
// right uid
|
|
|
|
treq.Spec.BoundObjectRef.UID = pod.UID
|
|
|
|
if _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
// wrong uid
|
|
|
|
treq.Spec.BoundObjectRef.UID = wrongUID
|
|
|
|
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err == nil {
|
|
|
|
t.Fatalf("expected err creating token bound to pod with wrong uid but got: %#v", resp)
|
|
|
|
}
|
|
|
|
// no uid
|
|
|
|
treq.Spec.BoundObjectRef.UID = noUID
|
|
|
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
|
|
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-pod"`, "kubernetes.io", "pod", "name")
|
|
|
|
checkPayload(t, treq.Status.Token, "null", "kubernetes.io", "secret")
|
|
|
|
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
2018-02-25 04:14:59 +00:00
|
|
|
|
2018-03-28 17:49:15 +00:00
|
|
|
info := doTokenReview(t, cs, treq, false)
|
|
|
|
if len(info.Extra) != 2 {
|
|
|
|
t.Fatalf("expected Extra have length of 2 but was length %d: %#v", len(info.Extra), info.Extra)
|
|
|
|
}
|
|
|
|
if expected := map[string]authenticationv1.ExtraValue{
|
|
|
|
"authentication.kubernetes.io/pod-name": {pod.ObjectMeta.Name},
|
|
|
|
"authentication.kubernetes.io/pod-uid": {string(pod.ObjectMeta.UID)},
|
|
|
|
}; !reflect.DeepEqual(info.Extra, expected) {
|
|
|
|
t.Fatalf("unexpected Extra:\ngot:\t%#v\nwant:\t%#v", info.Extra, expected)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
delPod()
|
|
|
|
doTokenReview(t, cs, treq, true)
|
2018-02-23 19:40:43 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("bound to service account and secret", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
2018-05-17 19:47:27 +00:00
|
|
|
Audiences: []string{"api"},
|
2018-02-23 19:40:43 +00:00
|
|
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
|
|
|
Kind: "Secret",
|
|
|
|
APIVersion: "v1",
|
|
|
|
Name: secret.Name,
|
|
|
|
UID: secret.UID,
|
|
|
|
},
|
2018-02-06 04:53:25 +00:00
|
|
|
},
|
2018-02-23 19:40:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err == nil {
|
|
|
|
t.Fatalf("expected err creating token for nonexistant svcacct but got: %#v", resp)
|
|
|
|
}
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err == nil {
|
|
|
|
t.Fatalf("expected err creating token bound to nonexistant secret but got: %#v", resp)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
secret, delSecret := createDeleteSecret(t, cs, secret)
|
|
|
|
defer delSecret()
|
2018-02-23 19:40:43 +00:00
|
|
|
|
|
|
|
// right uid
|
|
|
|
treq.Spec.BoundObjectRef.UID = secret.UID
|
|
|
|
if _, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
// wrong uid
|
|
|
|
treq.Spec.BoundObjectRef.UID = wrongUID
|
|
|
|
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err == nil {
|
|
|
|
t.Fatalf("expected err creating token bound to secret with wrong uid but got: %#v", resp)
|
|
|
|
}
|
|
|
|
// no uid
|
|
|
|
treq.Spec.BoundObjectRef.UID = noUID
|
|
|
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
2018-02-06 04:53:25 +00:00
|
|
|
|
2018-02-23 19:40:43 +00:00
|
|
|
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
|
|
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
|
|
|
checkPayload(t, treq.Status.Token, `null`, "kubernetes.io", "pod")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-secret"`, "kubernetes.io", "secret", "name")
|
|
|
|
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
2018-02-25 04:14:59 +00:00
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, false)
|
|
|
|
delSecret()
|
|
|
|
doTokenReview(t, cs, treq, true)
|
2018-02-23 19:40:43 +00:00
|
|
|
})
|
2018-02-23 22:47:54 +00:00
|
|
|
|
|
|
|
t.Run("bound to service account and pod running as different service account", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
2018-05-17 19:47:27 +00:00
|
|
|
Audiences: []string{"api"},
|
2018-02-23 22:47:54 +00:00
|
|
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
|
|
|
Kind: "Pod",
|
|
|
|
APIVersion: "v1",
|
|
|
|
Name: otherpod.Name,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
_, del = createDeletePod(t, cs, otherpod)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
if resp, err := cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err == nil {
|
|
|
|
t.Fatalf("expected err but got: %#v", resp)
|
|
|
|
}
|
|
|
|
})
|
2018-02-25 04:14:59 +00:00
|
|
|
|
|
|
|
t.Run("expired token", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
2018-05-17 19:47:27 +00:00
|
|
|
Audiences: []string{"api"},
|
2018-02-25 04:14:59 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, false)
|
2018-05-17 19:47:27 +00:00
|
|
|
|
|
|
|
// backdate the token
|
|
|
|
then := time.Now().Add(-2 * time.Hour)
|
|
|
|
sc := &jwt.Claims{
|
|
|
|
Subject: apiserverserviceaccount.MakeUsername(sa.Namespace, sa.Name),
|
|
|
|
Audience: jwt.Audience([]string{"api"}),
|
|
|
|
IssuedAt: jwt.NewNumericDate(then),
|
|
|
|
NotBefore: jwt.NewNumericDate(then),
|
|
|
|
Expiry: jwt.NewNumericDate(then.Add(time.Duration(60*60) * time.Second)),
|
|
|
|
}
|
|
|
|
coresa := core.ServiceAccount{
|
|
|
|
ObjectMeta: sa.ObjectMeta,
|
|
|
|
}
|
|
|
|
_, pc := serviceaccount.Claims(coresa, nil, nil, 0, nil)
|
|
|
|
tok, err := masterConfig.ExtraConfig.ServiceAccountIssuer.GenerateToken(sc, pc)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err signing expired token: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
treq.Status.Token = tok
|
2018-02-25 04:14:59 +00:00
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("a token without an api audience is invalid", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
|
|
|
Audiences: []string{"not-the-api"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("a tokenrequest without an audience is valid against the api", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{},
|
|
|
|
}
|
|
|
|
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, false)
|
|
|
|
})
|
2018-04-23 07:31:54 +00:00
|
|
|
|
|
|
|
t.Run("a token should be invalid after recreating same name pod", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
2018-05-17 19:47:27 +00:00
|
|
|
Audiences: []string{"api"},
|
2018-04-23 07:31:54 +00:00
|
|
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
|
|
|
Kind: "Pod",
|
|
|
|
APIVersion: "v1",
|
|
|
|
Name: pod.Name,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
originalPod, originalDelPod := createDeletePod(t, cs, pod)
|
|
|
|
defer originalDelPod()
|
|
|
|
|
|
|
|
treq.Spec.BoundObjectRef.UID = originalPod.UID
|
|
|
|
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
|
|
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-pod"`, "kubernetes.io", "pod", "name")
|
|
|
|
checkPayload(t, treq.Status.Token, "null", "kubernetes.io", "secret")
|
|
|
|
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, false)
|
|
|
|
originalDelPod()
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
|
|
|
|
_, recreateDelPod := createDeletePod(t, cs, pod)
|
|
|
|
defer recreateDelPod()
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("a token should be invalid after recreating same name secret", func(t *testing.T) {
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
2018-05-17 19:47:27 +00:00
|
|
|
Audiences: []string{"api"},
|
2018-04-23 07:31:54 +00:00
|
|
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
|
|
|
Kind: "Secret",
|
|
|
|
APIVersion: "v1",
|
|
|
|
Name: secret.Name,
|
|
|
|
UID: secret.UID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
originalSecret, originalDelSecret := createDeleteSecret(t, cs, secret)
|
|
|
|
defer originalDelSecret()
|
|
|
|
|
|
|
|
treq.Spec.BoundObjectRef.UID = originalSecret.UID
|
|
|
|
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
|
|
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
|
|
|
checkPayload(t, treq.Status.Token, `null`, "kubernetes.io", "pod")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-secret"`, "kubernetes.io", "secret", "name")
|
|
|
|
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, false)
|
|
|
|
originalDelSecret()
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
|
|
|
|
_, recreateDelSecret := createDeleteSecret(t, cs, secret)
|
|
|
|
defer recreateDelSecret()
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
})
|
2018-05-10 06:29:43 +00:00
|
|
|
|
|
|
|
t.Run("a token request within expiration time", func(t *testing.T) {
|
|
|
|
normalExpirationTime := maxExpirationSeconds - 10*60
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
|
|
|
Audiences: []string{"api"},
|
|
|
|
ExpirationSeconds: &normalExpirationTime,
|
|
|
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
|
|
|
Kind: "Secret",
|
|
|
|
APIVersion: "v1",
|
|
|
|
Name: secret.Name,
|
|
|
|
UID: secret.UID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
originalSecret, originalDelSecret := createDeleteSecret(t, cs, secret)
|
|
|
|
defer originalDelSecret()
|
|
|
|
|
|
|
|
treq.Spec.BoundObjectRef.UID = originalSecret.UID
|
|
|
|
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
|
|
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
|
|
|
checkPayload(t, treq.Status.Token, `null`, "kubernetes.io", "pod")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-secret"`, "kubernetes.io", "secret", "name")
|
|
|
|
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
|
|
|
checkExpiration(t, treq, normalExpirationTime)
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, false)
|
|
|
|
originalDelSecret()
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
|
|
|
|
_, recreateDelSecret := createDeleteSecret(t, cs, secret)
|
|
|
|
defer recreateDelSecret()
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("a token request with out-of-range expiration", func(t *testing.T) {
|
|
|
|
tooLongExpirationTime := maxExpirationSeconds + 10*60
|
|
|
|
treq := &authenticationv1.TokenRequest{
|
|
|
|
Spec: authenticationv1.TokenRequestSpec{
|
|
|
|
Audiences: []string{"api"},
|
|
|
|
ExpirationSeconds: &tooLongExpirationTime,
|
|
|
|
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
|
|
|
Kind: "Secret",
|
|
|
|
APIVersion: "v1",
|
|
|
|
Name: secret.Name,
|
|
|
|
UID: secret.UID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
sa, del := createDeleteSvcAcct(t, cs, sa)
|
|
|
|
defer del()
|
|
|
|
|
|
|
|
originalSecret, originalDelSecret := createDeleteSecret(t, cs, secret)
|
|
|
|
defer originalDelSecret()
|
|
|
|
|
|
|
|
treq.Spec.BoundObjectRef.UID = originalSecret.UID
|
|
|
|
if treq, err = cs.CoreV1().ServiceAccounts(sa.Namespace).CreateToken(sa.Name, treq); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkPayload(t, treq.Status.Token, `"system:serviceaccount:myns:test-svcacct"`, "sub")
|
|
|
|
checkPayload(t, treq.Status.Token, `["api"]`, "aud")
|
|
|
|
checkPayload(t, treq.Status.Token, `null`, "kubernetes.io", "pod")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-secret"`, "kubernetes.io", "secret", "name")
|
|
|
|
checkPayload(t, treq.Status.Token, `"myns"`, "kubernetes.io", "namespace")
|
|
|
|
checkPayload(t, treq.Status.Token, `"test-svcacct"`, "kubernetes.io", "serviceaccount", "name")
|
|
|
|
checkExpiration(t, treq, maxExpirationSeconds)
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, false)
|
|
|
|
originalDelSecret()
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
|
|
|
|
_, recreateDelSecret := createDeleteSecret(t, cs, secret)
|
|
|
|
defer recreateDelSecret()
|
|
|
|
|
|
|
|
doTokenReview(t, cs, treq, true)
|
|
|
|
})
|
2018-02-25 04:14:59 +00:00
|
|
|
}
|
|
|
|
|
2018-03-28 17:49:15 +00:00
|
|
|
func doTokenReview(t *testing.T, cs clientset.Interface, treq *authenticationv1.TokenRequest, expectErr bool) authenticationv1.UserInfo {
|
2018-02-25 04:14:59 +00:00
|
|
|
t.Helper()
|
|
|
|
trev, err := cs.AuthenticationV1().TokenReviews().Create(&authenticationv1.TokenReview{
|
|
|
|
Spec: authenticationv1.TokenReviewSpec{
|
|
|
|
Token: treq.Status.Token,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
t.Logf("status: %+v", trev.Status)
|
|
|
|
if (trev.Status.Error != "") && !expectErr {
|
|
|
|
t.Fatalf("expected no error but got: %v", trev.Status.Error)
|
|
|
|
}
|
|
|
|
if (trev.Status.Error == "") && expectErr {
|
|
|
|
t.Fatalf("expected error but got: %+v", trev.Status)
|
|
|
|
}
|
|
|
|
if !trev.Status.Authenticated && !expectErr {
|
|
|
|
t.Fatal("expected token to be authenticated but it wasn't")
|
|
|
|
}
|
2018-03-28 17:49:15 +00:00
|
|
|
return trev.Status.User
|
2018-02-06 04:53:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-10 06:29:43 +00:00
|
|
|
func checkExpiration(t *testing.T, treq *authenticationv1.TokenRequest, expectedExpiration int64) {
|
|
|
|
t.Helper()
|
|
|
|
if treq.Spec.ExpirationSeconds == nil {
|
|
|
|
t.Errorf("unexpected nil expiration seconds.")
|
|
|
|
}
|
|
|
|
if *treq.Spec.ExpirationSeconds != expectedExpiration {
|
|
|
|
t.Errorf("unexpected expiration seconds.\nsaw:\t%d\nwant:\t%d", treq.Spec.ExpirationSeconds, expectedExpiration)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-06 04:53:25 +00:00
|
|
|
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)
|
|
|
|
}
|
2018-02-23 19:40:43 +00:00
|
|
|
|
|
|
|
func createDeleteSvcAcct(t *testing.T, cs clientset.Interface, sa *v1.ServiceAccount) (*v1.ServiceAccount, func()) {
|
|
|
|
t.Helper()
|
|
|
|
sa, err := cs.CoreV1().ServiceAccounts(sa.Namespace).Create(sa)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
done := false
|
2018-02-23 19:40:43 +00:00
|
|
|
return sa, func() {
|
|
|
|
t.Helper()
|
2018-02-25 04:14:59 +00:00
|
|
|
if done {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
done = true
|
2018-02-23 19:40:43 +00:00
|
|
|
if err := cs.CoreV1().ServiceAccounts(sa.Namespace).Delete(sa.Name, nil); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func createDeletePod(t *testing.T, cs clientset.Interface, pod *v1.Pod) (*v1.Pod, func()) {
|
|
|
|
t.Helper()
|
|
|
|
pod, err := cs.CoreV1().Pods(pod.Namespace).Create(pod)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
done := false
|
2018-02-23 19:40:43 +00:00
|
|
|
return pod, func() {
|
|
|
|
t.Helper()
|
2018-02-25 04:14:59 +00:00
|
|
|
if done {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
done = true
|
2018-02-23 19:40:43 +00:00
|
|
|
if err := cs.CoreV1().Pods(pod.Namespace).Delete(pod.Name, nil); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func createDeleteSecret(t *testing.T, cs clientset.Interface, sec *v1.Secret) (*v1.Secret, func()) {
|
|
|
|
t.Helper()
|
|
|
|
sec, err := cs.CoreV1().Secrets(sec.Namespace).Create(sec)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
2018-02-25 04:14:59 +00:00
|
|
|
done := false
|
2018-02-23 19:40:43 +00:00
|
|
|
return sec, func() {
|
|
|
|
t.Helper()
|
2018-02-25 04:14:59 +00:00
|
|
|
if done {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
done = true
|
2018-02-23 19:40:43 +00:00
|
|
|
if err := cs.CoreV1().Secrets(sec.Namespace).Delete(sec.Name, nil); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-12-06 19:00:53 +00:00
|
|
|
|
|
|
|
func newIndexer(get func(namespace, name string) (interface{}, error)) cache.Indexer {
|
|
|
|
return &fakeIndexer{get: get}
|
|
|
|
}
|
|
|
|
|
|
|
|
type fakeIndexer struct {
|
|
|
|
cache.Indexer
|
|
|
|
get func(namespace, name string) (interface{}, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *fakeIndexer) GetByKey(key string) (interface{}, bool, error) {
|
|
|
|
parts := strings.SplitN(key, "/", 2)
|
|
|
|
namespace := parts[0]
|
|
|
|
name := ""
|
|
|
|
if len(parts) == 2 {
|
|
|
|
name = parts[1]
|
|
|
|
}
|
|
|
|
obj, err := f.get(namespace, name)
|
|
|
|
return obj, err == nil, err
|
|
|
|
}
|