Split the serviceaccount package into two parts

Public utility methods and JWT parsing, and controller specific logic.
Also remove the coupling between ServiceAccountTokenGetter and the
authenticator class.
This commit is contained in:
Clayton Coleman
2015-12-24 16:54:40 -05:00
parent 3d5ed379b0
commit 9dad7e624c
12 changed files with 84 additions and 74 deletions

View File

@@ -1,214 +0,0 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 (
"bytes"
"crypto/rsa"
"errors"
"fmt"
"io/ioutil"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/auth/authenticator"
"k8s.io/kubernetes/pkg/auth/user"
jwt "github.com/dgrijalva/jwt-go"
"github.com/golang/glog"
)
const (
Issuer = "kubernetes/serviceaccount"
SubjectClaim = "sub"
IssuerClaim = "iss"
ServiceAccountNameClaim = "kubernetes.io/serviceaccount/service-account.name"
ServiceAccountUIDClaim = "kubernetes.io/serviceaccount/service-account.uid"
SecretNameClaim = "kubernetes.io/serviceaccount/secret.name"
NamespaceClaim = "kubernetes.io/serviceaccount/namespace"
)
type TokenGenerator interface {
// GenerateToken generates a token which will identify the given ServiceAccount.
// The returned token will be stored in the given (and yet-unpersisted) Secret.
GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error)
}
// ReadPrivateKey is a helper function for reading an rsa.PrivateKey from a PEM-encoded file
func ReadPrivateKey(file string) (*rsa.PrivateKey, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
return jwt.ParseRSAPrivateKeyFromPEM(data)
}
// ReadPublicKey is a helper function for reading an rsa.PublicKey from a PEM-encoded file
// Reads public keys from both public and private key files
func ReadPublicKey(file string) (*rsa.PublicKey, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil {
return &privateKey.PublicKey, nil
}
return jwt.ParseRSAPublicKeyFromPEM(data)
}
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.
// privateKey is a PEM-encoded byte array of a private RSA key.
// JWTTokenAuthenticator()
func JWTTokenGenerator(key *rsa.PrivateKey) TokenGenerator {
return &jwtTokenGenerator{key}
}
type jwtTokenGenerator struct {
key *rsa.PrivateKey
}
func (j *jwtTokenGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
token := jwt.New(jwt.SigningMethodRS256)
// Identify the issuer
token.Claims[IssuerClaim] = Issuer
// Username
token.Claims[SubjectClaim] = MakeUsername(serviceAccount.Namespace, serviceAccount.Name)
// Persist enough structured info for the authenticator to be able to look up the service account and secret
token.Claims[NamespaceClaim] = serviceAccount.Namespace
token.Claims[ServiceAccountNameClaim] = serviceAccount.Name
token.Claims[ServiceAccountUIDClaim] = serviceAccount.UID
token.Claims[SecretNameClaim] = secret.Name
// Sign and get the complete encoded token as a string
return token.SignedString(j.key)
}
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
// Token signatures are verified using each of the given public keys until one works (allowing key rotation)
// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified with the provided ServiceAccountTokenGetter
func JWTTokenAuthenticator(keys []*rsa.PublicKey, lookup bool, getter ServiceAccountTokenGetter) authenticator.Token {
return &jwtTokenAuthenticator{keys, lookup, getter}
}
type jwtTokenAuthenticator struct {
keys []*rsa.PublicKey
lookup bool
getter ServiceAccountTokenGetter
}
func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) {
var validationError error
for i, key := range j.keys {
// Attempt to verify with each key until we find one that works
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return key, nil
})
if err != nil {
switch err := err.(type) {
case *jwt.ValidationError:
if (err.Errors & jwt.ValidationErrorMalformed) != 0 {
// Not a JWT, no point in continuing
return nil, false, nil
}
if (err.Errors & jwt.ValidationErrorSignatureInvalid) != 0 {
// Signature error, perhaps one of the other keys will verify the signature
// If not, we want to return this error
glog.V(4).Infof("Signature error (key %d): %v", i, err)
validationError = err
continue
}
}
// Other errors should just return as errors
return nil, false, err
}
// If we get here, we have a token with a recognized signature
// Make sure we issued the token
iss, _ := parsedToken.Claims[IssuerClaim].(string)
if iss != Issuer {
return nil, false, nil
}
// Make sure the claims we need exist
sub, _ := parsedToken.Claims[SubjectClaim].(string)
if len(sub) == 0 {
return nil, false, errors.New("sub claim is missing")
}
namespace, _ := parsedToken.Claims[NamespaceClaim].(string)
if len(namespace) == 0 {
return nil, false, errors.New("namespace claim is missing")
}
secretName, _ := parsedToken.Claims[SecretNameClaim].(string)
if len(namespace) == 0 {
return nil, false, errors.New("secretName claim is missing")
}
serviceAccountName, _ := parsedToken.Claims[ServiceAccountNameClaim].(string)
if len(serviceAccountName) == 0 {
return nil, false, errors.New("serviceAccountName claim is missing")
}
serviceAccountUID, _ := parsedToken.Claims[ServiceAccountUIDClaim].(string)
if len(serviceAccountUID) == 0 {
return nil, false, errors.New("serviceAccountUID claim is missing")
}
subjectNamespace, subjectName, err := SplitUsername(sub)
if err != nil || subjectNamespace != namespace || subjectName != serviceAccountName {
return nil, false, errors.New("sub claim is invalid")
}
if j.lookup {
// Make sure token hasn't been invalidated by deletion of the secret
secret, err := j.getter.GetSecret(namespace, secretName)
if err != nil {
glog.V(4).Infof("Could not retrieve token %s/%s for service account %s/%s: %v", namespace, secretName, namespace, serviceAccountName, err)
return nil, false, errors.New("Token has been invalidated")
}
if bytes.Compare(secret.Data[api.ServiceAccountTokenKey], []byte(token)) != 0 {
glog.V(4).Infof("Token contents no longer matches %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
return nil, false, errors.New("Token does not match server's copy")
}
// Make sure service account still exists (name and UID)
serviceAccount, err := j.getter.GetServiceAccount(namespace, serviceAccountName)
if err != nil {
glog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, serviceAccountName, err)
return nil, false, err
}
if string(serviceAccount.UID) != serviceAccountUID {
glog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, serviceAccountName, string(serviceAccount.UID), serviceAccountUID)
return nil, false, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
}
}
return UserInfo(namespace, serviceAccountName, serviceAccountUID), true, nil
}
return nil, false, validationError
}

View File

@@ -1,272 +0,0 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 (
"crypto/rsa"
"io/ioutil"
"os"
"reflect"
"testing"
"github.com/dgrijalva/jwt-go"
"k8s.io/kubernetes/pkg/api"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
)
const otherPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArXz0QkIG1B5Bj2/W69GH
rsm5e+RC3kE+VTgocge0atqlLBek35tRqLgUi3AcIrBZ/0YctMSWDVcRt5fkhWwe
Lqjj6qvAyNyOkrkBi1NFDpJBjYJtuKHgRhNxXbOzTSNpdSKXTfOkzqv56MwHOP25
yP/NNAODUtr92D5ySI5QX8RbXW+uDn+ixul286PBW/BCrE4tuS88dA0tYJPf8LCu
sqQOwlXYH/rNUg4Pyl9xxhR5DIJR0OzNNfChjw60zieRIt2LfM83fXhwk8IxRGkc
gPZm7ZsipmfbZK2Tkhnpsa4QxDg7zHJPMsB5kxRXW0cQipXcC3baDyN9KBApNXa0
PwIDAQAB
-----END PUBLIC KEY-----`
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249XwEo9k4tM8fMxV7zx
OhcrP+WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecI
zshKuv1gKIxbbLQMOuK1eA/4HALyEkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG
51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJU
j7OTh/AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEi
B4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRM
WwIDAQAB
-----END PUBLIC KEY-----
`
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA249XwEo9k4tM8fMxV7zxOhcrP+WvXn917koM5Qr2ZXs4vo26
e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecIzshKuv1gKIxbbLQMOuK1eA/4HALy
EkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0T
ctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh/AjjCnMnkgvKT2tpKxYQ59P
gDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp
1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRMWwIDAQABAoIBAHJx8GqyCBDNbqk7
e7/hI9iE1S10Wwol5GH2RWxqX28cYMKq+8aE2LI1vPiXO89xOgelk4DN6urX6xjK
ZBF8RRIMQy/e/O2F4+3wl+Nl4vOXV1u6iVXMsD6JRg137mqJf1Fr9elg1bsaRofL
Q7CxPoB8dhS+Qb+hj0DhlqhgA9zG345CQCAds0ZYAZe8fP7bkwrLqZpMn7Dz9WVm
++YgYYKjuE95kPuup/LtWfA9rJyE/Fws8/jGvRSpVn1XglMLSMKhLd27sE8ZUSV0
2KUzbfRGE0+AnRULRrjpYaPu0XQ2JjdNvtkjBnv27RB89W9Gklxq821eH1Y8got8
FZodjxECgYEA93pz7AQZ2xDs67d1XLCzpX84GxKzttirmyj3OIlxgzVHjEMsvw8v
sjFiBU5xEEQDosrBdSknnlJqyiq1YwWG/WDckr13d8G2RQWoySN7JVmTQfXcLoTu
YGRiiTuoEi3ab3ZqrgGrFgX7T/cHuasbYvzCvhM2b4VIR3aSxU2DTUMCgYEA4x7J
T/ErP6GkU5nKstu/mIXwNzayEO1BJvPYsy7i7EsxTm3xe/b8/6cYOz5fvJLGH5mT
Q8YvuLqBcMwZardrYcwokD55UvNLOyfADDFZ6l3WntIqbA640Ok2g1X4U8J09xIq
ZLIWK1yWbbvi4QCeN5hvWq47e8sIj5QHjIIjRwkCgYEAyNqjltxFN9zmzPDa2d24
EAvOt3pYTYBQ1t9KtqImdL0bUqV6fZ6PsWoPCgt+DBuHb+prVPGP7Bkr/uTmznU/
+AlTO+12NsYLbr2HHagkXE31DEXE7CSLa8RNjN/UKtz4Ohq7vnowJvG35FCz/mb3
FUHbtHTXa2+bGBUOTf/5Hw0CgYBxw0r9EwUhw1qnUYJ5op7OzFAtp+T7m4ul8kCa
SCL8TxGsgl+SQ34opE775dtYfoBk9a0RJqVit3D8yg71KFjOTNAIqHJm/Vyyjc+h
i9rJDSXiuczsAVfLtPVMRfS0J9QkqeG4PIfkQmVLI/CZ2ZBmsqEcX+eFs4ZfPLun
Qsxe2QKBgGuPilIbLeIBDIaPiUI0FwU8v2j8CEQBYvoQn34c95hVQsig/o5z7zlo
UsO0wlTngXKlWdOcCs1kqEhTLrstf48djDxAYAxkw40nzeJOt7q52ib/fvf4/UBy
X024wzbiw1q07jFCyfQmODzURAx1VNT7QVUMdz/N8vy47/H40AZJ
-----END RSA PRIVATE KEY-----
`
func getPrivateKey(data string) *rsa.PrivateKey {
key, _ := jwt.ParseRSAPrivateKeyFromPEM([]byte(data))
return key
}
func getPublicKey(data string) *rsa.PublicKey {
key, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(data))
return key
}
func TestReadPrivateKey(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(privateKey), os.FileMode(0600)); err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
if _, err := ReadPrivateKey(f.Name()); err != nil {
t.Fatalf("error reading key: %v", err)
}
}
func TestReadPublicKey(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(publicKey), os.FileMode(0600)); err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
if _, err := ReadPublicKey(f.Name()); err != nil {
t.Fatalf("error reading key: %v", err)
}
}
func TestTokenGenerateAndValidate(t *testing.T) {
expectedUserName := "system:serviceaccount:test:my-service-account"
expectedUserUID := "12345"
// Related API objects
serviceAccount := &api.ServiceAccount{
ObjectMeta: api.ObjectMeta{
Name: "my-service-account",
UID: "12345",
Namespace: "test",
},
}
secret := &api.Secret{
ObjectMeta: api.ObjectMeta{
Name: "my-secret",
Namespace: "test",
},
}
// Generate the token
generator := JWTTokenGenerator(getPrivateKey(privateKey))
token, err := generator.GenerateToken(*serviceAccount, *secret)
if err != nil {
t.Fatalf("error generating token: %v", err)
}
if len(token) == 0 {
t.Fatalf("no token generated")
}
// "Save" the token
secret.Data = map[string][]byte{
"token": []byte(token),
}
testCases := map[string]struct {
Client client.Interface
Keys []*rsa.PublicKey
ExpectedErr bool
ExpectedOK bool
ExpectedUserName string
ExpectedUserUID string
ExpectedGroups []string
}{
"no keys": {
Client: nil,
Keys: []*rsa.PublicKey{},
ExpectedErr: false,
ExpectedOK: false,
},
"invalid keys": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
"valid key": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"},
},
"rotated keys": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey), getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"},
},
"valid lookup": {
Client: testclient.NewSimpleFake(serviceAccount, secret),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"},
},
"invalid secret lookup": {
Client: testclient.NewSimpleFake(serviceAccount),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
"invalid serviceaccount lookup": {
Client: testclient.NewSimpleFake(secret),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
}
for k, tc := range testCases {
getter := NewGetterFromClient(tc.Client)
authenticator := JWTTokenAuthenticator(tc.Keys, tc.Client != nil, getter)
user, ok, err := authenticator.AuthenticateToken(token)
if (err != nil) != tc.ExpectedErr {
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
continue
}
if ok != tc.ExpectedOK {
t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok)
continue
}
if err != nil || !ok {
continue
}
if user.GetName() != tc.ExpectedUserName {
t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, user.GetName())
continue
}
if user.GetUID() != tc.ExpectedUserUID {
t.Errorf("%s: Expected userUID=%v, got %v", k, tc.ExpectedUserUID, user.GetUID())
continue
}
if !reflect.DeepEqual(user.GetGroups(), tc.ExpectedGroups) {
t.Errorf("%s: Expected groups=%v, got %v", k, tc.ExpectedGroups, user.GetGroups())
continue
}
}
}
func TestMakeSplitUsername(t *testing.T) {
username := MakeUsername("ns", "name")
ns, name, err := SplitUsername(username)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
if ns != "ns" || name != "name" {
t.Errorf("Expected ns/name, got %s/%s", ns, name)
}
invalid := []string{"test", "system:serviceaccount", "system:serviceaccount:", "system:serviceaccount:ns", "system:serviceaccount:ns:name:extra"}
for _, n := range invalid {
_, _, err := SplitUsername("test")
if err == nil {
t.Errorf("Expected error for %s", n)
}
}
}

View File

@@ -22,17 +22,12 @@ import (
"k8s.io/kubernetes/pkg/registry/generic"
"k8s.io/kubernetes/pkg/registry/secret"
secretetcd "k8s.io/kubernetes/pkg/registry/secret/etcd"
"k8s.io/kubernetes/pkg/registry/serviceaccount"
serviceaccountregistry "k8s.io/kubernetes/pkg/registry/serviceaccount"
serviceaccountetcd "k8s.io/kubernetes/pkg/registry/serviceaccount/etcd"
"k8s.io/kubernetes/pkg/serviceaccount"
"k8s.io/kubernetes/pkg/storage"
)
// ServiceAccountTokenGetter defines functions to retrieve a named service account and secret
type ServiceAccountTokenGetter interface {
GetServiceAccount(namespace, name string) (*api.ServiceAccount, error)
GetSecret(namespace, name string) (*api.Secret, error)
}
// clientGetter implements ServiceAccountTokenGetter using a client.Interface
type clientGetter struct {
client client.Interface
@@ -42,7 +37,7 @@ type clientGetter struct {
// uses the specified client to retrieve service accounts and secrets.
// The client should NOT authenticate using a service account token
// the returned getter will be used to retrieve, or recursion will result.
func NewGetterFromClient(c client.Interface) ServiceAccountTokenGetter {
func NewGetterFromClient(c client.Interface) serviceaccount.ServiceAccountTokenGetter {
return clientGetter{c}
}
func (c clientGetter) GetServiceAccount(namespace, name string) (*api.ServiceAccount, error) {
@@ -54,13 +49,13 @@ func (c clientGetter) GetSecret(namespace, name string) (*api.Secret, error) {
// registryGetter implements ServiceAccountTokenGetter using a service account and secret registry
type registryGetter struct {
serviceAccounts serviceaccount.Registry
serviceAccounts serviceaccountregistry.Registry
secrets secret.Registry
}
// NewGetterFromRegistries returns a ServiceAccountTokenGetter that
// uses the specified registries to retrieve service accounts and secrets.
func NewGetterFromRegistries(serviceAccounts serviceaccount.Registry, secrets secret.Registry) ServiceAccountTokenGetter {
func NewGetterFromRegistries(serviceAccounts serviceaccountregistry.Registry, secrets secret.Registry) serviceaccount.ServiceAccountTokenGetter {
return &registryGetter{serviceAccounts, secrets}
}
func (r *registryGetter) GetServiceAccount(namespace, name string) (*api.ServiceAccount, error) {
@@ -74,9 +69,9 @@ func (r *registryGetter) GetSecret(namespace, name string) (*api.Secret, error)
// NewGetterFromStorageInterface returns a ServiceAccountTokenGetter that
// uses the specified storage to retrieve service accounts and secrets.
func NewGetterFromStorageInterface(s storage.Interface) ServiceAccountTokenGetter {
func NewGetterFromStorageInterface(s storage.Interface) serviceaccount.ServiceAccountTokenGetter {
return NewGetterFromRegistries(
serviceaccount.NewRegistry(serviceaccountetcd.NewREST(s, generic.UndecoratedStorage)),
serviceaccountregistry.NewRegistry(serviceaccountetcd.NewREST(s, generic.UndecoratedStorage)),
secret.NewRegistry(secretetcd.NewREST(s, generic.UndecoratedStorage)),
)
}

View File

@@ -30,6 +30,7 @@ import (
"k8s.io/kubernetes/pkg/fields"
"k8s.io/kubernetes/pkg/registry/secret"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/serviceaccount"
"k8s.io/kubernetes/pkg/util/sets"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/pkg/watch"
@@ -40,7 +41,7 @@ const NumServiceAccountRemoveReferenceRetries = 10
// TokensControllerOptions contains options for the TokensController
type TokensControllerOptions struct {
// TokenGenerator is the generator to use to create new tokens
TokenGenerator TokenGenerator
TokenGenerator serviceaccount.TokenGenerator
// ServiceAccountResync is the time.Duration at which to fully re-list service accounts.
// If zero, re-list will be delayed as long as possible
ServiceAccountResync time.Duration
@@ -111,7 +112,7 @@ type TokensController struct {
stopChan chan struct{}
client client.Interface
token TokenGenerator
token serviceaccount.TokenGenerator
rootCA []byte
@@ -451,7 +452,7 @@ func (e *TokensController) getServiceAccount(secret *api.Secret, fetchOnCacheMis
for _, obj := range namespaceAccounts {
serviceAccount := obj.(*api.ServiceAccount)
if IsServiceAccountToken(secret, serviceAccount) {
if serviceaccount.IsServiceAccountToken(secret, serviceAccount) {
return serviceAccount, nil
}
}
@@ -465,7 +466,7 @@ func (e *TokensController) getServiceAccount(secret *api.Secret, fetchOnCacheMis
return nil, err
}
if IsServiceAccountToken(secret, serviceAccount) {
if serviceaccount.IsServiceAccountToken(secret, serviceAccount) {
return serviceAccount, nil
}
}
@@ -486,7 +487,7 @@ func (e *TokensController) listTokenSecrets(serviceAccount *api.ServiceAccount)
for _, obj := range namespaceSecrets {
secret := obj.(*api.Secret)
if IsServiceAccountToken(secret, serviceAccount) {
if serviceaccount.IsServiceAccountToken(secret, serviceAccount) {
items = append(items, secret)
}
}

View File

@@ -1,104 +0,0 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 (
"fmt"
"strings"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/validation"
"k8s.io/kubernetes/pkg/auth/user"
)
const (
ServiceAccountUsernamePrefix = "system:serviceaccount:"
ServiceAccountUsernameSeparator = ":"
ServiceAccountGroupPrefix = "system:serviceaccounts:"
AllServiceAccountsGroup = "system:serviceaccounts"
)
// MakeUsername generates a username from the given namespace and ServiceAccount name.
// The resulting username can be passed to SplitUsername to extract the original namespace and ServiceAccount name.
func MakeUsername(namespace, name string) string {
return ServiceAccountUsernamePrefix + namespace + ServiceAccountUsernameSeparator + name
}
var invalidUsernameErr = fmt.Errorf("Username must be in the form %s", MakeUsername("namespace", "name"))
// SplitUsername returns the namespace and ServiceAccount name embedded in the given username,
// or an error if the username is not a valid name produced by MakeUsername
func SplitUsername(username string) (string, string, error) {
if !strings.HasPrefix(username, ServiceAccountUsernamePrefix) {
return "", "", invalidUsernameErr
}
trimmed := strings.TrimPrefix(username, ServiceAccountUsernamePrefix)
parts := strings.Split(trimmed, ServiceAccountUsernameSeparator)
if len(parts) != 2 {
return "", "", invalidUsernameErr
}
namespace, name := parts[0], parts[1]
if ok, _ := validation.ValidateNamespaceName(namespace, false); !ok {
return "", "", invalidUsernameErr
}
if ok, _ := validation.ValidateServiceAccountName(name, false); !ok {
return "", "", invalidUsernameErr
}
return namespace, name, nil
}
// MakeGroupNames generates service account group names for the given namespace and ServiceAccount name
func MakeGroupNames(namespace, name string) []string {
return []string{
AllServiceAccountsGroup,
MakeNamespaceGroupName(namespace),
}
}
// MakeNamespaceGroupName returns the name of the group all service accounts in the namespace are included in
func MakeNamespaceGroupName(namespace string) string {
return ServiceAccountGroupPrefix + namespace
}
// UserInfo returns a user.Info interface for the given namespace, service account name and UID
func UserInfo(namespace, name, uid string) user.Info {
return &user.DefaultInfo{
Name: MakeUsername(namespace, name),
UID: uid,
Groups: MakeGroupNames(namespace, name),
}
}
// IsServiceAccountToken returns true if the secret is a valid api token for the service account
func IsServiceAccountToken(secret *api.Secret, sa *api.ServiceAccount) bool {
if secret.Type != api.SecretTypeServiceAccountToken {
return false
}
name := secret.Annotations[api.ServiceAccountNameKey]
uid := secret.Annotations[api.ServiceAccountUIDKey]
if name != sa.Name {
// Name must match
return false
}
if len(uid) > 0 && uid != string(sa.UID) {
// If UID is specified, it must match
return false
}
return true
}

View File

@@ -1,82 +0,0 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 "testing"
func TestMakeUsername(t *testing.T) {
testCases := map[string]struct {
Namespace string
Name string
ExpectedErr bool
}{
"valid": {
Namespace: "foo",
Name: "bar",
ExpectedErr: false,
},
"empty": {
ExpectedErr: true,
},
"empty namespace": {
Namespace: "",
Name: "foo",
ExpectedErr: true,
},
"empty name": {
Namespace: "foo",
Name: "",
ExpectedErr: true,
},
"extra segments": {
Namespace: "foo",
Name: "bar:baz",
ExpectedErr: true,
},
"invalid chars in namespace": {
Namespace: "foo ",
Name: "bar",
ExpectedErr: true,
},
"invalid chars in name": {
Namespace: "foo",
Name: "bar ",
ExpectedErr: true,
},
}
for k, tc := range testCases {
username := MakeUsername(tc.Namespace, tc.Name)
namespace, name, err := SplitUsername(username)
if (err != nil) != tc.ExpectedErr {
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
continue
}
if err != nil {
continue
}
if namespace != tc.Namespace {
t.Errorf("%s: Expected namespace %q, got %q", k, tc.Namespace, namespace)
}
if name != tc.Name {
t.Errorf("%s: Expected name %q, got %q", k, tc.Name, name)
}
}
}