ServiceAccount e2e/integration tests

pull/6/head
Jordan Liggitt 2015-05-06 16:05:00 -04:00
parent 7e14a80f63
commit 92bd58ede6
3 changed files with 672 additions and 0 deletions

View File

@ -50,6 +50,12 @@ type Authorizer interface {
Authorize(a Attributes) (err error)
}
type AuthorizerFunc func(a Attributes) error
func (f AuthorizerFunc) Authorize(a Attributes) error {
return f(a)
}
// AttributesRecord implements Attributes interface.
type AttributesRecord struct {
User user.Info

View File

@ -0,0 +1,101 @@
/*
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 e2e
import (
"fmt"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount"
. "github.com/onsi/ginkgo"
)
var _ = Describe("ServiceAccounts", func() {
var c *client.Client
var ns string
BeforeEach(func() {
var err error
c, err = loadClient()
expectNoError(err)
ns_, err := createTestingNS("service-accounts", c)
ns = ns_.Name
expectNoError(err)
})
AfterEach(func() {
// Clean up the namespace if a non-default one was used
if ns != api.NamespaceDefault {
By("Cleaning up the namespace")
err := c.Namespaces().Delete(ns)
expectNoError(err)
}
})
It("should mount an API token into pods", func() {
var tokenName string
var tokenContent string
// Standard get, update retry loop
expectNoError(wait.Poll(time.Millisecond*500, time.Second*10, func() (bool, error) {
By("getting the auto-created API token")
tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
secrets, err := c.Secrets(ns).List(labels.Everything(), tokenSelector)
if err != nil {
return false, err
}
if len(secrets.Items) == 0 {
return false, nil
}
if len(secrets.Items) > 1 {
return false, fmt.Errorf("Expected 1 token secret, got %d", len(secrets.Items))
}
tokenName = secrets.Items[0].Name
tokenContent = string(secrets.Items[0].Data[api.ServiceAccountTokenKey])
return true, nil
}))
pod := &api.Pod{
ObjectMeta: api.ObjectMeta{
Name: "pod-service-account-" + string(util.NewUUID()),
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "service-account-test",
Image: "kubernetes/mounttest:0.1",
Args: []string{
fmt.Sprintf("--file_content=%s/%s", serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountTokenKey),
},
},
},
RestartPolicy: api.RestartPolicyNever,
},
}
testContainerOutputInNamespace("consume service account token", c, pod, []string{
fmt.Sprintf(`content of file "%s/%s": %s`, serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountTokenKey, tokenContent),
}, ns)
})
})

View File

@ -0,0 +1,565 @@
// +build integration,!no-etcd
/*
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 integration
// This file tests authentication and (soon) authorization of HTTP requests to a master object.
// It does not use the client in pkg/client/... because authentication and authorization needs
// to work for any client of the HTTP interface.
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/master"
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools/etcdtest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
serviceaccountadmission "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount"
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/union"
)
const (
rootUserName = "root"
rootToken = "root-user-token"
readOnlyServiceAccountName = "ro"
readWriteServiceAccountName = "rw"
)
func init() {
requireEtcd()
}
func TestServiceAccountAutoCreate(t *testing.T) {
c, _, stopFunc := startServiceAccountTestServer(t)
defer stopFunc()
ns := "test-service-account-creation"
// Create namespace
_, err := c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: ns}})
if err != nil {
t.Fatalf("could not create namespace: %v", err)
}
// Get service account
defaultUser, err := getServiceAccount(c, ns, "default", true)
if err != nil {
t.Fatalf("Default serviceaccount not created: %v", err)
}
// Delete service account
err = c.ServiceAccounts(ns).Delete(defaultUser.Name)
if err != nil {
t.Fatalf("Could not delete default serviceaccount: %v", err)
}
// Get recreated service account
defaultUser2, err := getServiceAccount(c, ns, "default", true)
if err != nil {
t.Fatalf("Default serviceaccount not created: %v", err)
}
if defaultUser2.UID == defaultUser.UID {
t.Fatalf("Expected different UID with recreated serviceaccount")
}
}
func TestServiceAccountTokenAutoCreate(t *testing.T) {
c, _, stopFunc := startServiceAccountTestServer(t)
defer stopFunc()
ns := "test-service-account-token-creation"
name := "my-service-account"
// Create namespace
_, err := c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: ns}})
if err != nil {
t.Fatalf("could not create namespace: %v", err)
}
// Create service account
serviceAccount, err := c.ServiceAccounts(ns).Create(&api.ServiceAccount{ObjectMeta: api.ObjectMeta{Name: name}})
if err != nil {
t.Fatalf("Service Account not created: %v", err)
}
// Get token
token1Name, token1, err := getReferencedServiceAccountToken(c, ns, name, true)
if err != nil {
t.Fatal(err)
}
// Delete token
err = c.Secrets(ns).Delete(token1Name)
if err != nil {
t.Fatalf("Could not delete token: %v", err)
}
// Get recreated token
token2Name, token2, err := getReferencedServiceAccountToken(c, ns, name, true)
if err != nil {
t.Fatal(err)
}
if token1Name == token2Name {
t.Fatalf("Expected new auto-created token name")
}
if token1 == token2 {
t.Fatalf("Expected new auto-created token value")
}
// Trigger creation of a new referenced token
serviceAccount, err = c.ServiceAccounts(ns).Get(name)
if err != nil {
t.Fatal(err)
}
serviceAccount.Secrets = []api.ObjectReference{}
_, err = c.ServiceAccounts(ns).Update(serviceAccount)
if err != nil {
t.Fatal(err)
}
// Get rotated token
token3Name, token3, err := getReferencedServiceAccountToken(c, ns, name, true)
if err != nil {
t.Fatal(err)
}
if token3Name == token2Name {
t.Fatalf("Expected new auto-created token name")
}
if token3 == token2 {
t.Fatalf("Expected new auto-created token value")
}
// Delete service account
err = c.ServiceAccounts(ns).Delete(name)
if err != nil {
t.Fatal(err)
}
// Wait for tokens to be deleted
tokensToCleanup := util.NewStringSet(token1Name, token2Name, token3Name)
err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) {
// Get all secrets in the namespace
secrets, err := c.Secrets(ns).List(labels.Everything(), fields.Everything())
// Retrieval errors should fail
if err != nil {
return false, err
}
for _, s := range secrets.Items {
if tokensToCleanup.Has(s.Name) {
// Still waiting for tokens to be cleaned up
return false, nil
}
}
// All clean
return true, nil
})
if err != nil {
t.Fatalf("Error waiting for tokens to be deleted: %v", err)
}
}
func TestServiceAccountTokenAutoMount(t *testing.T) {
c, _, stopFunc := startServiceAccountTestServer(t)
defer stopFunc()
ns := "auto-mount-ns"
// Create "my" namespace
_, err := c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: ns}})
if err != nil && !errors.IsAlreadyExists(err) {
t.Fatalf("could not create namespace: %v", err)
}
// Get default token
defaultTokenName, _, err := getReferencedServiceAccountToken(c, ns, serviceaccountadmission.DefaultServiceAccountName, true)
if err != nil {
t.Fatal(err)
}
// Pod to create
protoPod := api.Pod{
ObjectMeta: api.ObjectMeta{Name: "protopod"},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "container-1",
Image: "container-1-image",
},
{
Name: "container-2",
Image: "container-2-image",
VolumeMounts: []api.VolumeMount{
{Name: "empty-dir", MountPath: serviceaccountadmission.DefaultAPITokenMountPath},
},
},
},
Volumes: []api.Volume{
{
Name: "empty-dir",
VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}},
},
},
},
}
// Pod we expect to get created
expectedServiceAccount := serviceaccountadmission.DefaultServiceAccountName
expectedVolumes := append(protoPod.Spec.Volumes, api.Volume{
Name: defaultTokenName,
VolumeSource: api.VolumeSource{
Secret: &api.SecretVolumeSource{
SecretName: defaultTokenName,
},
},
})
expectedContainer1VolumeMounts := []api.VolumeMount{
{Name: defaultTokenName, MountPath: serviceaccountadmission.DefaultAPITokenMountPath, ReadOnly: true},
}
expectedContainer2VolumeMounts := protoPod.Spec.Containers[1].VolumeMounts
createdPod, err := c.Pods(ns).Create(&protoPod)
if err != nil {
t.Fatal(err)
}
if createdPod.Spec.ServiceAccount != expectedServiceAccount {
t.Fatalf("Expected %s, got %s", expectedServiceAccount, createdPod.Spec.ServiceAccount)
}
if !api.Semantic.DeepEqual(&expectedVolumes, &createdPod.Spec.Volumes) {
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedVolumes, createdPod.Spec.Volumes)
}
if !api.Semantic.DeepEqual(&expectedContainer1VolumeMounts, &createdPod.Spec.Containers[0].VolumeMounts) {
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedContainer1VolumeMounts, createdPod.Spec.Containers[0].VolumeMounts)
}
if !api.Semantic.DeepEqual(&expectedContainer2VolumeMounts, &createdPod.Spec.Containers[1].VolumeMounts) {
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedContainer2VolumeMounts, createdPod.Spec.Containers[1].VolumeMounts)
}
}
func TestServiceAccountTokenAuthentication(t *testing.T) {
c, config, stopFunc := startServiceAccountTestServer(t)
defer stopFunc()
myns := "auth-ns"
otherns := "other-ns"
// Create "my" namespace
_, err := c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: myns}})
if err != nil && !errors.IsAlreadyExists(err) {
t.Fatalf("could not create namespace: %v", err)
}
// Create "other" namespace
_, err = c.Namespaces().Create(&api.Namespace{ObjectMeta: api.ObjectMeta{Name: otherns}})
if err != nil && !errors.IsAlreadyExists(err) {
t.Fatalf("could not create namespace: %v", err)
}
// Create "ro" user in myns
_, err = c.ServiceAccounts(myns).Create(&api.ServiceAccount{ObjectMeta: api.ObjectMeta{Name: readOnlyServiceAccountName}})
if err != nil {
t.Fatalf("Service Account not created: %v", err)
}
roTokenName, roToken, err := getReferencedServiceAccountToken(c, myns, readOnlyServiceAccountName, true)
if err != nil {
t.Fatal(err)
}
roClientConfig := config
roClientConfig.BearerToken = roToken
roClient := client.NewOrDie(&roClientConfig)
doServiceAccountAPIRequests(t, roClient, myns, true, true, false)
doServiceAccountAPIRequests(t, roClient, otherns, true, false, false)
err = c.Secrets(myns).Delete(roTokenName)
if err != nil {
t.Fatalf("could not delete token: %v", err)
}
doServiceAccountAPIRequests(t, roClient, myns, false, false, false)
// Create "rw" user in myns
_, err = c.ServiceAccounts(myns).Create(&api.ServiceAccount{ObjectMeta: api.ObjectMeta{Name: readWriteServiceAccountName}})
if err != nil {
t.Fatalf("Service Account not created: %v", err)
}
_, rwToken, err := getReferencedServiceAccountToken(c, myns, readWriteServiceAccountName, true)
if err != nil {
t.Fatal(err)
}
rwClientConfig := config
rwClientConfig.BearerToken = rwToken
rwClient := client.NewOrDie(&rwClientConfig)
doServiceAccountAPIRequests(t, rwClient, myns, true, true, true)
doServiceAccountAPIRequests(t, rwClient, otherns, true, false, false)
// Get default user and token which should have been automatically created
_, defaultToken, err := getReferencedServiceAccountToken(c, myns, "default", true)
if err != nil {
t.Fatalf("could not get default user and token: %v", err)
}
defaultClientConfig := config
defaultClientConfig.BearerToken = defaultToken
defaultClient := client.NewOrDie(&defaultClientConfig)
doServiceAccountAPIRequests(t, defaultClient, myns, true, false, false)
}
// startServiceAccountTestServer returns a started server
// It is the responsibility of the caller to ensure the returned stopFunc is called
func startServiceAccountTestServer(t *testing.T) (*client.Client, client.Config, func()) {
deleteAllEtcdKeys()
// Etcd
helper, err := master.NewEtcdHelper(newEtcdClient(), testapi.Version(), etcdtest.PathPrefix())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Listener
var m *master.Master
apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
m.Handler.ServeHTTP(w, req)
}))
// Anonymous client config
clientConfig := client.Config{Host: apiServer.URL, Version: testapi.Version()}
// Root client
rootClient := client.NewOrDie(&client.Config{Host: apiServer.URL, Version: testapi.Version(), BearerToken: rootToken})
// Set up two authenticators:
// 1. A token authenticator that maps the rootToken to the "root" user
// 2. A ServiceAccountToken authenticator that validates ServiceAccount tokens
rootTokenAuth := authenticator.TokenFunc(func(token string) (user.Info, bool, error) {
if token == rootToken {
return &user.DefaultInfo{rootUserName, "", []string{}}, true, nil
}
return nil, false, nil
})
serviceAccountKey, err := rsa.GenerateKey(rand.Reader, 2048)
serviceAccountTokenAuth := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{&serviceAccountKey.PublicKey}, true, rootClient)
authenticator := union.New(
bearertoken.New(rootTokenAuth),
bearertoken.New(serviceAccountTokenAuth),
)
// Set up a stub authorizer:
// 1. The "root" user is allowed to do anything
// 2. ServiceAccounts named "ro" are allowed read-only operations in their namespace
// 3. ServiceAccounts named "rw" are allowed any operation in their namespace
authorizer := authorizer.AuthorizerFunc(func(attrs authorizer.Attributes) error {
username := attrs.GetUserName()
ns := attrs.GetNamespace()
// If the user is "root"...
if username == rootUserName {
// allow them to do anything
return nil
}
// If the user is a service account...
if serviceAccountNamespace, serviceAccountName, err := serviceaccount.SplitUsername(username); err == nil {
// Limit them to their own namespace
if serviceAccountNamespace == ns {
switch serviceAccountName {
case readOnlyServiceAccountName:
if attrs.IsReadOnly() {
return nil
}
case readWriteServiceAccountName:
return nil
}
}
}
return fmt.Errorf("User %s is denied (ns=%s, readonly=%v, resource=%s)", username, ns, attrs.IsReadOnly(), attrs.GetResource())
})
// Set up admission plugin to auto-assign serviceaccounts to pods
serviceAccountAdmission := serviceaccountadmission.NewServiceAccount(rootClient)
// Create a master and install handlers into mux.
m = master.New(&master.Config{
EtcdHelper: helper,
KubeletClient: client.FakeKubeletClient{},
EnableLogsSupport: false,
EnableUISupport: false,
EnableIndex: true,
APIPrefix: "/api",
Authenticator: authenticator,
Authorizer: authorizer,
AdmissionControl: serviceAccountAdmission,
})
// Start the service account and service account token controllers
tokenController := serviceaccount.NewTokensController(rootClient, serviceaccount.DefaultTokenControllerOptions(serviceaccount.JWTTokenGenerator(serviceAccountKey)))
tokenController.Run()
serviceAccountController := serviceaccount.NewServiceAccountsController(rootClient, serviceaccount.DefaultServiceAccountControllerOptions())
serviceAccountController.Run()
// Start the admission plugin reflectors
serviceAccountAdmission.Run()
stop := func() {
tokenController.Stop()
serviceAccountController.Stop()
serviceAccountAdmission.Stop()
apiServer.Close()
}
return rootClient, clientConfig, stop
}
func getServiceAccount(c *client.Client, ns string, name string, shouldWait bool) (*api.ServiceAccount, error) {
if !shouldWait {
return c.ServiceAccounts(ns).Get(name)
}
var user *api.ServiceAccount
var err error
err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) {
user, err = c.ServiceAccounts(ns).Get(name)
if errors.IsNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
})
return user, err
}
func getReferencedServiceAccountToken(c *client.Client, ns string, name string, shouldWait bool) (string, string, error) {
tokenName := ""
token := ""
findToken := func() (bool, error) {
user, err := c.ServiceAccounts(ns).Get(name)
if errors.IsNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
for _, ref := range user.Secrets {
secret, err := c.Secrets(ns).Get(ref.Name)
if errors.IsNotFound(err) {
continue
}
if err != nil {
return false, err
}
if secret.Type != api.SecretTypeServiceAccountToken {
continue
}
name := secret.Annotations[api.ServiceAccountNameKey]
uid := secret.Annotations[api.ServiceAccountUIDKey]
tokenData := secret.Data[api.ServiceAccountTokenKey]
if name == user.Name && uid == string(user.UID) && len(tokenData) > 0 {
tokenName = secret.Name
token = string(tokenData)
return true, nil
}
}
return false, nil
}
if shouldWait {
err := wait.Poll(time.Second, 10*time.Second, findToken)
if err != nil {
return "", "", err
}
} else {
ok, err := findToken()
if err != nil {
return "", "", err
}
if !ok {
return "", "", fmt.Errorf("No token found for %s/%s", ns, name)
}
}
return tokenName, token, nil
}
type testOperation func() error
func doServiceAccountAPIRequests(t *testing.T, c *client.Client, ns string, authenticated bool, canRead bool, canWrite bool) {
testSecret := &api.Secret{
ObjectMeta: api.ObjectMeta{Name: "testSecret"},
Data: map[string][]byte{"test": []byte("data")},
}
readOps := []testOperation{
func() error { _, err := c.Secrets(ns).List(labels.Everything(), fields.Everything()); return err },
func() error { _, err := c.Pods(ns).List(labels.Everything(), fields.Everything()); return err },
}
writeOps := []testOperation{
func() error { _, err := c.Secrets(ns).Create(testSecret); return err },
func() error { return c.Secrets(ns).Delete(testSecret.Name) },
}
for _, op := range readOps {
err := op()
unauthorizedError := errors.IsUnauthorized(err)
forbiddenError := errors.IsForbidden(err)
switch {
case !authenticated && !unauthorizedError:
t.Fatalf("expected unauthorized error, got %v", err)
case authenticated && unauthorizedError:
t.Fatalf("unexpected unauthorized error: %v", err)
case authenticated && canRead && forbiddenError:
t.Fatalf("unexpected forbidden error: %v", err)
case authenticated && !canRead && !forbiddenError:
t.Fatalf("expected forbidden error, got: %v", err)
}
}
for _, op := range writeOps {
err := op()
unauthorizedError := errors.IsUnauthorized(err)
forbiddenError := errors.IsForbidden(err)
switch {
case !authenticated && !unauthorizedError:
t.Fatalf("expected unauthorized error, got %v", err)
case authenticated && unauthorizedError:
t.Fatalf("unexpected unauthorized error: %v", err)
case authenticated && canWrite && forbiddenError:
t.Fatalf("unexpected forbidden error: %v", err)
case authenticated && !canWrite && !forbiddenError:
t.Fatalf("expected forbidden error, got: %v", err)
}
}
}