mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-06 07:57:35 +00:00
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:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ®istryGetter{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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user