kubernetes/test/integration/serviceaccount/service_account_test.go
2022-10-24 09:37:53 -07:00

695 lines
23 KiB
Go

/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
// This file tests authentication and (soon) authorization of HTTP requests to an API server 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 (
"context"
"fmt"
"sync"
"testing"
"time"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
serviceaccountapiserver "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authorization/authorizer"
unionauthz "k8s.io/apiserver/pkg/authorization/union"
utilfeature "k8s.io/apiserver/pkg/util/feature"
clientinformers "k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/util/keyutil"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/pkg/controller"
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
"k8s.io/kubernetes/pkg/controlplane"
"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
kubefeatures "k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/serviceaccount"
serviceaccountadmission "k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
"k8s.io/kubernetes/test/integration/framework"
)
const (
readOnlyServiceAccountName = "ro"
readWriteServiceAccountName = "rw"
dateFormat = "2006-01-02"
)
func TestServiceAccountAutoCreate(t *testing.T) {
c, _, stopFunc, err := startServiceAccountTestServerAndWaitForCaches(t)
defer stopFunc()
if err != nil {
t.Fatalf("failed to setup ServiceAccounts server: %v", err)
}
ns := "test-service-account-creation"
// Create namespace
_, err = c.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}, metav1.CreateOptions{})
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.CoreV1().ServiceAccounts(ns).Delete(context.TODO(), defaultUser.Name, metav1.DeleteOptions{})
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) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.LegacyServiceAccountTokenNoAutoGeneration, false)()
c, _, stopFunc, err := startServiceAccountTestServerAndWaitForCaches(t)
defer stopFunc()
if err != nil {
t.Fatalf("failed to setup ServiceAccounts server: %v", err)
}
ns := "test-service-account-token-creation"
name := "my-service-account"
// Create namespace
_, err = c.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("could not create namespace: %v", err)
}
// Create service account
_, err = c.CoreV1().ServiceAccounts(ns).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: name}}, metav1.CreateOptions{})
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.CoreV1().Secrets(ns).Delete(context.TODO(), token1Name, metav1.DeleteOptions{})
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.CoreV1().ServiceAccounts(ns).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
serviceAccount.Secrets = []v1.ObjectReference{}
_, err = c.CoreV1().ServiceAccounts(ns).Update(context.TODO(), serviceAccount, metav1.UpdateOptions{})
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.CoreV1().ServiceAccounts(ns).Delete(context.TODO(), name, metav1.DeleteOptions{})
if err != nil {
t.Fatal(err)
}
// Wait for tokens to be deleted
tokensToCleanup := sets.NewString(token1Name, token2Name, token3Name)
err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) {
// Get all secrets in the namespace
secrets, err := c.CoreV1().Secrets(ns).List(context.TODO(), metav1.ListOptions{})
// 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, err := startServiceAccountTestServerAndWaitForCaches(t)
defer stopFunc()
if err != nil {
t.Fatalf("failed to setup ServiceAccounts server: %v", err)
}
ns := "auto-mount-ns"
// Create "my" namespace
_, err = c.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
t.Fatalf("could not create namespace: %v", err)
}
// Pod to create
protoPod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "protopod"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "container",
Image: "container-image",
},
},
},
}
createdPod, err := c.CoreV1().Pods(ns).Create(context.TODO(), &protoPod, metav1.CreateOptions{})
if err != nil {
t.Fatal(err)
}
expectedServiceAccount := serviceaccountadmission.DefaultServiceAccountName
if createdPod.Spec.ServiceAccountName != expectedServiceAccount {
t.Fatalf("Expected %s, got %s", expectedServiceAccount, createdPod.Spec.ServiceAccountName)
}
if len(createdPod.Spec.Volumes) == 0 || createdPod.Spec.Volumes[0].Projected == nil {
t.Fatal("Expected projected volume for service account token inserted")
}
}
func TestServiceAccountTokenAuthentication(t *testing.T) {
c, config, stopFunc, err := startServiceAccountTestServerAndWaitForCaches(t)
defer stopFunc()
if err != nil {
t.Fatalf("failed to setup ServiceAccounts server: %v", err)
}
myns := "auth-ns"
otherns := "other-ns"
// Create "my" namespace
_, err = c.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: myns}}, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
t.Fatalf("could not create namespace: %v", err)
}
// Create "other" namespace
_, err = c.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: otherns}}, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
t.Fatalf("could not create namespace: %v", err)
}
// Create "ro" user in myns
roSA, err := c.CoreV1().ServiceAccounts(myns).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: readOnlyServiceAccountName}}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Service Account not created: %v", err)
}
roTokenName := "ro-test-token"
secret, err := createServiceAccountToken(c, roSA, myns, roTokenName)
if err != nil {
t.Fatalf("Secret not created: %v", err)
}
roClientConfig := *config
roClientConfig.BearerToken = string(secret.Data[v1.ServiceAccountTokenKey])
roClient := clientset.NewForConfigOrDie(&roClientConfig)
doServiceAccountAPIRequests(t, roClient, myns, true, true, false)
doServiceAccountAPIRequests(t, roClient, otherns, true, false, false)
err = c.CoreV1().Secrets(myns).Delete(context.TODO(), roTokenName, metav1.DeleteOptions{})
if err != nil {
t.Fatalf("could not delete token: %v", err)
}
// wait for delete to be observed and reacted to via watch
err = wait.PollImmediate(100*time.Millisecond, 30*time.Second, func() (bool, error) {
_, err := roClient.CoreV1().Secrets(myns).List(context.TODO(), metav1.ListOptions{})
if err == nil {
t.Logf("token is still valid, waiting")
return false, nil
}
if !apierrors.IsUnauthorized(err) {
t.Logf("expected unauthorized error, got %v", err)
return false, nil
}
return true, nil
})
if err != nil {
t.Fatalf("waiting for token to be invalidated: %v", err)
}
doServiceAccountAPIRequests(t, roClient, myns, false, false, false)
// Create "rw" user in myns
rwSA, err := c.CoreV1().ServiceAccounts(myns).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: readWriteServiceAccountName}}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Service Account not created: %v", err)
}
rwTokenName := "rw-test-token"
secret, err = createServiceAccountToken(c, rwSA, myns, rwTokenName)
if err != nil {
t.Fatalf("Secret not created: %v", err)
}
rwClientConfig := *config
rwClientConfig.BearerToken = string(secret.Data[v1.ServiceAccountTokenKey])
rwClient := clientset.NewForConfigOrDie(&rwClientConfig)
doServiceAccountAPIRequests(t, rwClient, myns, true, true, true)
doServiceAccountAPIRequests(t, rwClient, otherns, true, false, false)
}
func TestLegacyServiceAccountTokenTracking(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.LegacyServiceAccountTokenNoAutoGeneration, false)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, kubefeatures.LegacyServiceAccountTokenTracking, true)()
c, config, stopFunc, err := startServiceAccountTestServerAndWaitForCaches(t)
defer stopFunc()
if err != nil {
t.Fatalf("failed to setup ServiceAccounts server: %v", err)
}
// create service account
myns := "auth-ns"
_, err = c.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: myns}}, metav1.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
t.Fatalf("could not create namespace: %v", err)
}
mysa, err := c.CoreV1().ServiceAccounts(myns).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: readOnlyServiceAccountName}}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Service Account not created: %v", err)
}
manualSecretName := "manual-token"
manualSecret, err := createServiceAccountToken(c, mysa, myns, manualSecretName)
if err != nil {
t.Fatalf("Secret not created: %v", err)
}
autoSecretName, autoSecretTokenData, err := getReferencedServiceAccountToken(c, myns, readOnlyServiceAccountName, true)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
secretName string
secretTokenData string
expectWarning bool
}{
{
name: "manually created legacy token",
secretName: manualSecretName,
secretTokenData: string(manualSecret.Data[v1.ServiceAccountTokenKey]),
},
{
name: "auto created legacy token",
secretName: autoSecretName,
secretTokenData: autoSecretTokenData,
expectWarning: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
myConfig := *config
wh := &warningHandler{}
myConfig.WarningHandler = wh
myConfig.BearerToken = string(test.secretTokenData)
roClient := clientset.NewForConfigOrDie(&myConfig)
dateBefore := time.Now().UTC().Format(dateFormat)
go func() {
doServiceAccountAPIRequests(t, roClient, myns, true, true, false)
}()
doServiceAccountAPIRequests(t, roClient, myns, true, true, false)
dateAfter := time.Now().UTC().Format(dateFormat)
liveSecret, err := c.CoreV1().Secrets(myns).Get(context.TODO(), test.secretName, metav1.GetOptions{})
if err != nil {
t.Fatalf("Could not get secret: %v", err)
}
if test.expectWarning && len(wh.warnings) != 8 {
t.Fatalf("Expect 8 warnings, got %d", len(wh.warnings))
}
if !test.expectWarning && len(wh.warnings) != 0 {
t.Fatalf("Don't expect warnings, got %d", len(wh.warnings))
}
// authenticated legacy token should have the expected annotation and label.
date, ok := liveSecret.GetLabels()[serviceaccount.LastUsedLabelKey]
if !ok {
t.Fatalf("Secret wasn't labeled with %q", serviceaccount.LastUsedLabelKey)
}
if date != dateBefore || date != dateAfter {
t.Fatalf("Secret was labeled with wrong date: %q", date)
}
})
}
// configmap should exist with 'since' timestamp.
if err = wait.PollImmediate(time.Millisecond*10, wait.ForeverTestTimeout, func() (bool, error) {
dateBefore := time.Now().UTC().Format("2006-01-02")
configMap, err := c.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(context.TODO(), legacytokentracking.ConfigMapName, metav1.GetOptions{})
if err != nil {
return false, fmt.Errorf("failed to get %q configmap, err %w", legacytokentracking.ConfigMapDataKey, err)
}
dateAfter := time.Now().UTC().Format("2006-01-02")
date, ok := configMap.Data[legacytokentracking.ConfigMapDataKey]
if !ok {
return false, fmt.Errorf("configMap doesn't contain key %q", legacytokentracking.ConfigMapDataKey)
}
if date != dateBefore || date != dateAfter {
return false, fmt.Errorf("configMap contains a wrong date %q", date)
}
return true, nil
}); err != nil {
t.Fatal(err)
}
}
// startServiceAccountTestServerAndWaitForCaches returns a started server
// It is the responsibility of the caller to ensure the returned stopFunc is called
func startServiceAccountTestServerAndWaitForCaches(t *testing.T) (clientset.Interface, *restclient.Config, func(), error) {
var serviceAccountKey interface{}
// Set up a API server
rootClientset, clientConfig, tearDownFn := framework.StartTestServer(t, framework.TestServerSetup{
ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
var err error
serviceAccountKey, err = keyutil.PrivateKeyFromFile(opts.ServiceAccountSigningKeyFile)
if err != nil {
t.Fatal(err)
}
},
ModifyServerConfig: func(config *controlplane.Config) {
// 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(ctx context.Context, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
username := ""
if user := attrs.GetUser(); user != nil {
username = user.GetName()
}
ns := attrs.GetNamespace()
// If the user is a service account...
if serviceAccountNamespace, serviceAccountName, err := serviceaccountapiserver.SplitUsername(username); err == nil {
// Limit them to their own namespace
if serviceAccountNamespace == ns {
switch serviceAccountName {
case readOnlyServiceAccountName:
if attrs.IsReadOnly() {
return authorizer.DecisionAllow, "", nil
}
case readWriteServiceAccountName:
return authorizer.DecisionAllow, "", nil
}
}
}
return authorizer.DecisionNoOpinion, fmt.Sprintf("User %s is denied (ns=%s, readonly=%v, resource=%s)", username, ns, attrs.IsReadOnly(), attrs.GetResource()), nil
})
config.GenericConfig.Authorization.Authorizer = unionauthz.New(config.GenericConfig.Authorization.Authorizer, authorizer)
},
})
ctx, cancel := context.WithCancel(context.Background())
stop := func() {
cancel()
tearDownFn()
}
informers := clientinformers.NewSharedInformerFactory(rootClientset, controller.NoResyncPeriodFunc())
// Start the service account and service account token controllers
tokenGenerator, err := serviceaccount.JWTTokenGenerator(serviceaccount.LegacyIssuer, serviceAccountKey)
if err != nil {
return rootClientset, clientConfig, stop, err
}
tokenController, err := serviceaccountcontroller.NewTokensController(
informers.Core().V1().ServiceAccounts(),
informers.Core().V1().Secrets(),
rootClientset,
serviceaccountcontroller.TokensControllerOptions{
TokenGenerator: tokenGenerator,
AutoGenerate: !utilfeature.DefaultFeatureGate.Enabled(kubefeatures.LegacyServiceAccountTokenNoAutoGeneration),
},
)
if err != nil {
return rootClientset, clientConfig, stop, err
}
go tokenController.Run(1, ctx.Done())
serviceAccountController, err := serviceaccountcontroller.NewServiceAccountsController(
informers.Core().V1().ServiceAccounts(),
informers.Core().V1().Namespaces(),
rootClientset,
serviceaccountcontroller.DefaultServiceAccountsControllerOptions(),
)
if err != nil {
return rootClientset, clientConfig, stop, err
}
informers.Start(ctx.Done())
go serviceAccountController.Run(ctx, 5)
// since this method starts the controllers in a separate goroutine
// and the tests don't check /readyz there is no way
// the tests can tell it is safe to call the server and requests won't be rejected
// thus we wait until caches have synced
informers.WaitForCacheSync(ctx.Done())
return rootClientset, clientConfig, stop, nil
}
func getServiceAccount(c clientset.Interface, ns string, name string, shouldWait bool) (*v1.ServiceAccount, error) {
if !shouldWait {
return c.CoreV1().ServiceAccounts(ns).Get(context.TODO(), name, metav1.GetOptions{})
}
var user *v1.ServiceAccount
var err error
err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) {
user, err = c.CoreV1().ServiceAccounts(ns).Get(context.TODO(), name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
})
return user, err
}
func createServiceAccountToken(c clientset.Interface, sa *v1.ServiceAccount, ns string, name string) (*v1.Secret, error) {
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Annotations: map[string]string{
v1.ServiceAccountNameKey: sa.GetName(),
v1.ServiceAccountUIDKey: string(sa.UID),
},
},
Type: v1.SecretTypeServiceAccountToken,
Data: map[string][]byte{},
}
secret, err := c.CoreV1().Secrets(ns).Create(context.TODO(), secret, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("failed to create secret (%s:%s) %+v, err: %v", ns, secret.Name, *secret, err)
}
err = wait.Poll(time.Second, 10*time.Second, func() (bool, error) {
if len(secret.Data[v1.ServiceAccountTokenKey]) != 0 {
return false, nil
}
secret, err = c.CoreV1().Secrets(ns).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return true, fmt.Errorf("failed to get secret (%s:%s) %+v, err: %v", ns, secret.Name, *secret, err)
}
return true, nil
})
return secret, nil
}
func getReferencedServiceAccountToken(c clientset.Interface, ns string, name string, shouldWait bool) (string, string, error) {
tokenName := ""
token := ""
findToken := func() (bool, error) {
user, err := c.CoreV1().ServiceAccounts(ns).Get(context.TODO(), name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
for _, ref := range user.Secrets {
secret, err := c.CoreV1().Secrets(ns).Get(context.TODO(), ref.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
continue
}
if err != nil {
return false, err
}
if secret.Type != v1.SecretTypeServiceAccountToken {
continue
}
name := secret.Annotations[v1.ServiceAccountNameKey]
uid := secret.Annotations[v1.ServiceAccountUIDKey]
tokenData := secret.Data[v1.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 clientset.Interface, ns string, authenticated bool, canRead bool, canWrite bool) {
testSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "testSecret"},
Data: map[string][]byte{"test": []byte("data")},
}
readOps := []testOperation{
func() error {
_, err := c.CoreV1().Secrets(ns).List(context.TODO(), metav1.ListOptions{})
return err
},
func() error {
_, err := c.CoreV1().Pods(ns).List(context.TODO(), metav1.ListOptions{})
return err
},
}
writeOps := []testOperation{
func() error {
_, err := c.CoreV1().Secrets(ns).Create(context.TODO(), testSecret, metav1.CreateOptions{})
return err
},
func() error {
return c.CoreV1().Secrets(ns).Delete(context.TODO(), testSecret.Name, metav1.DeleteOptions{})
},
}
for _, op := range readOps {
err := op()
unauthorizedError := apierrors.IsUnauthorized(err)
forbiddenError := apierrors.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 := apierrors.IsUnauthorized(err)
forbiddenError := apierrors.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)
}
}
}
type warningHandler struct {
mu sync.Mutex
warnings []string
}
func (r *warningHandler) HandleWarningHeader(code int, agent string, message string) {
r.mu.Lock()
defer r.mu.Unlock()
r.warnings = append(r.warnings, message)
}