From 5ef4fe959a45e423d2b992e9c21e6e9db4b950c5 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 4 Nov 2019 22:41:32 -0500 Subject: [PATCH] Switch kubelet/aggregated API servers to use v1 tokenreviews --- .../app/options/options_test.go | 7 +- cmd/kubelet/app/auth.go | 4 +- pkg/kubeapiserver/authenticator/config.go | 7 +- pkg/kubeapiserver/options/authentication.go | 6 + .../authenticatorfactory/delegating.go | 2 +- .../pkg/server/options/authentication.go | 2 +- .../token/webhook/round_trip_test.go | 87 +++ .../authenticator/token/webhook/webhook.go | 129 +++- .../{webhook_test.go => webhook_v1_test.go} | 134 ++-- .../token/webhook/webhook_v1beta1_test.go | 686 ++++++++++++++++++ test/integration/auth/auth_test.go | 2 +- 11 files changed, 958 insertions(+), 108 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/round_trip_test.go rename staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/{webhook_test.go => webhook_v1_test.go} (79%) create mode 100644 staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go diff --git a/cmd/kube-apiserver/app/options/options_test.go b/cmd/kube-apiserver/app/options/options_test.go index 8a94921714f..c2c390f01fe 100644 --- a/cmd/kube-apiserver/app/options/options_test.go +++ b/cmd/kube-apiserver/app/options/options_test.go @@ -22,9 +22,11 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/pflag" - "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apiserver/pkg/admission" apiserveroptions "k8s.io/apiserver/pkg/server/options" "k8s.io/apiserver/pkg/storage/storagebackend" auditbuffered "k8s.io/apiserver/plugin/pkg/audit/buffered" @@ -266,6 +268,7 @@ func TestAddFlags(t *testing.T) { WebHook: &kubeoptions.WebHookAuthenticationOptions{ CacheTTL: 180000000000, ConfigFile: "/token-webhook-config", + Version: "v1beta1", }, BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{}, OIDC: &kubeoptions.OIDCAuthenticationOptions{ @@ -305,6 +308,6 @@ func TestAddFlags(t *testing.T) { } if !reflect.DeepEqual(expected, s) { - t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", diff.ObjectReflectDiff(expected, s)) + t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}))) } } diff --git a/cmd/kubelet/app/auth.go b/cmd/kubelet/app/auth.go index eea8d2b1886..f75766daafc 100644 --- a/cmd/kubelet/app/auth.go +++ b/cmd/kubelet/app/auth.go @@ -28,7 +28,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizerfactory" "k8s.io/apiserver/pkg/server/dynamiccertificates" clientset "k8s.io/client-go/kubernetes" - authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1" + authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1" authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1" kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config" @@ -43,7 +43,7 @@ func BuildAuth(nodeName types.NodeName, client clientset.Interface, config kubel sarClient authorizationclient.SubjectAccessReviewInterface ) if client != nil && !reflect.ValueOf(client).IsNil() { - tokenClient = client.AuthenticationV1beta1().TokenReviews() + tokenClient = client.AuthenticationV1().TokenReviews() sarClient = client.AuthorizationV1beta1().SubjectAccessReviews() } diff --git a/pkg/kubeapiserver/authenticator/config.go b/pkg/kubeapiserver/authenticator/config.go index de068e9f670..0e27620d603 100644 --- a/pkg/kubeapiserver/authenticator/config.go +++ b/pkg/kubeapiserver/authenticator/config.go @@ -68,6 +68,7 @@ type Config struct { ServiceAccountIssuer string APIAudiences authenticator.Audiences WebhookTokenAuthnConfigFile string + WebhookTokenAuthnVersion string WebhookTokenAuthnCacheTTL time.Duration TokenSuccessCacheTTL time.Duration @@ -179,7 +180,7 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er tokenAuthenticators = append(tokenAuthenticators, oidcAuth) } if len(config.WebhookTokenAuthnConfigFile) > 0 { - webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL, config.APIAudiences) + webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnVersion, config.WebhookTokenAuthnCacheTTL, config.APIAudiences) if err != nil { return nil, nil, err } @@ -305,8 +306,8 @@ func newServiceAccountAuthenticator(iss string, keyfiles []string, apiAudiences return tokenAuthenticator, nil } -func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) { - webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, implicitAuds) +func newWebhookTokenAuthenticator(webhookConfigFile string, version string, ttl time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) { + webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, version, implicitAuds) if err != nil { return nil, err } diff --git a/pkg/kubeapiserver/options/authentication.go b/pkg/kubeapiserver/options/authentication.go index 1d4177802d8..1d528d97376 100644 --- a/pkg/kubeapiserver/options/authentication.go +++ b/pkg/kubeapiserver/options/authentication.go @@ -90,6 +90,7 @@ type TokenFileAuthenticationOptions struct { type WebHookAuthenticationOptions struct { ConfigFile string + Version string CacheTTL time.Duration } @@ -155,6 +156,7 @@ func (s *BuiltInAuthenticationOptions) WithTokenFile() *BuiltInAuthenticationOpt func (s *BuiltInAuthenticationOptions) WithWebHook() *BuiltInAuthenticationOptions { s.WebHook = &WebHookAuthenticationOptions{ + Version: "v1beta1", CacheTTL: 2 * time.Minute, } return s @@ -303,6 +305,9 @@ func (s *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { "File with webhook configuration for token authentication in kubeconfig format. "+ "The API server will query the remote service to determine authentication for bearer tokens.") + fs.StringVar(&s.WebHook.Version, "authentication-token-webhook-version", s.WebHook.Version, ""+ + "The API version of the authentication.k8s.io TokenReview to send to and expect from the webhook.") + fs.DurationVar(&s.WebHook.CacheTTL, "authentication-token-webhook-cache-ttl", s.WebHook.CacheTTL, "The duration to cache responses from the webhook token authenticator.") } @@ -370,6 +375,7 @@ func (s *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat if s.WebHook != nil { ret.WebhookTokenAuthnConfigFile = s.WebHook.ConfigFile + ret.WebhookTokenAuthnVersion = s.WebHook.Version ret.WebhookTokenAuthnCacheTTL = s.WebHook.CacheTTL if len(s.WebHook.ConfigFile) > 0 && s.WebHook.CacheTTL > 0 { diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go b/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go index 61cc68988f3..b9c7e2e6eee 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go @@ -32,7 +32,7 @@ import ( "k8s.io/apiserver/pkg/authentication/request/x509" "k8s.io/apiserver/pkg/authentication/token/cache" webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" - authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1" + authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1" ) // DelegatingAuthenticatorConfig is the minimal configuration needed to create an authenticator diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go b/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go index d90bbd66326..bdcca82194c 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go @@ -248,7 +248,7 @@ func (s *DelegatingAuthenticationOptions) ApplyTo(authenticationInfo *server.Aut // configure token review if client != nil { - cfg.TokenAccessReviewClient = client.AuthenticationV1beta1().TokenReviews() + cfg.TokenAccessReviewClient = client.AuthenticationV1().TokenReviews() } // look into configmaps/external-apiserver-authentication for missing authn info diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/round_trip_test.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/round_trip_test.go new file mode 100644 index 00000000000..c85196fd319 --- /dev/null +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/round_trip_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2019 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 webhook implements the authorizer.Authorizer interface using HTTP webhooks. +package webhook + +import ( + "math/rand" + "reflect" + "testing" + "time" + + fuzz "github.com/google/gofuzz" + + authenticationv1 "k8s.io/api/authentication/v1" + authenticationv1beta1 "k8s.io/api/authentication/v1beta1" + "k8s.io/apimachinery/pkg/util/diff" +) + +func TestRoundTrip(t *testing.T) { + f := fuzz.New() + seed := time.Now().UnixNano() + t.Logf("seed = %v", seed) + f.RandSource(rand.New(rand.NewSource(seed))) + + for i := 0; i < 1000; i++ { + original := &authenticationv1.TokenReview{} + f.Fuzz(&original.Spec) + f.Fuzz(&original.Status) + converted := &authenticationv1beta1.TokenReview{ + Spec: v1SpecToV1beta1Spec(&original.Spec), + Status: v1StatusToV1beta1Status(original.Status), + } + roundtripped := &authenticationv1.TokenReview{ + Spec: v1beta1SpecToV1Spec(converted.Spec), + Status: v1beta1StatusToV1Status(&converted.Status), + } + if !reflect.DeepEqual(original, roundtripped) { + t.Errorf("diff %s", diff.ObjectReflectDiff(original, roundtripped)) + } + } +} + +func v1StatusToV1beta1Status(in authenticationv1.TokenReviewStatus) authenticationv1beta1.TokenReviewStatus { + return authenticationv1beta1.TokenReviewStatus{ + Authenticated: in.Authenticated, + User: v1UserToV1beta1User(in.User), + Audiences: in.Audiences, + Error: in.Error, + } +} + +func v1UserToV1beta1User(u authenticationv1.UserInfo) authenticationv1beta1.UserInfo { + var extra map[string]authenticationv1beta1.ExtraValue + if u.Extra != nil { + extra = make(map[string]authenticationv1beta1.ExtraValue, len(u.Extra)) + for k, v := range u.Extra { + extra[k] = authenticationv1beta1.ExtraValue(v) + } + } + return authenticationv1beta1.UserInfo{ + Username: u.Username, + UID: u.UID, + Groups: u.Groups, + Extra: extra, + } +} + +func v1beta1SpecToV1Spec(in authenticationv1beta1.TokenReviewSpec) authenticationv1.TokenReviewSpec { + return authenticationv1.TokenReviewSpec{ + Token: in.Token, + Audiences: in.Audiences, + } +} diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go index 6359920628c..53c5d2285bd 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go @@ -20,30 +20,32 @@ package webhook import ( "context" "errors" + "fmt" "time" - authentication "k8s.io/api/authentication/v1beta1" + authenticationv1 "k8s.io/api/authentication/v1" + authenticationv1beta1 "k8s.io/api/authentication/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/kubernetes/scheme" - authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1" + authenticationv1client "k8s.io/client-go/kubernetes/typed/authentication/v1" "k8s.io/klog" ) -var ( - groupVersions = []schema.GroupVersion{authentication.SchemeGroupVersion} -) - const retryBackoff = 500 * time.Millisecond // Ensure WebhookTokenAuthenticator implements the authenticator.Token interface. var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil) +type tokenReviewer interface { + CreateContext(ctx context.Context, review *authenticationv1.TokenReview) (*authenticationv1.TokenReview, error) +} + type WebhookTokenAuthenticator struct { - tokenReview authenticationclient.TokenReviewInterface + tokenReview tokenReviewer initialBackoff time.Duration implicitAuds authenticator.Audiences } @@ -52,7 +54,7 @@ type WebhookTokenAuthenticator struct { // client. It is recommend to wrap this authenticator with the token cache // authenticator implemented in // k8s.io/apiserver/pkg/authentication/token/cache. -func NewFromInterface(tokenReview authenticationclient.TokenReviewInterface, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) { +func NewFromInterface(tokenReview authenticationv1client.TokenReviewInterface, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) { return newWithBackoff(tokenReview, retryBackoff, implicitAuds) } @@ -60,8 +62,8 @@ func NewFromInterface(tokenReview authenticationclient.TokenReviewInterface, imp // file. It is recommend to wrap this authenticator with the token cache // authenticator implemented in // k8s.io/apiserver/pkg/authentication/token/cache. -func New(kubeConfigFile string, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) { - tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile) +func New(kubeConfigFile string, version string, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) { + tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile, version) if err != nil { return nil, err } @@ -69,7 +71,7 @@ func New(kubeConfigFile string, implicitAuds authenticator.Audiences) (*WebhookT } // newWithBackoff allows tests to skip the sleep. -func newWithBackoff(tokenReview authenticationclient.TokenReviewInterface, initialBackoff time.Duration, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) { +func newWithBackoff(tokenReview tokenReviewer, initialBackoff time.Duration, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) { return &WebhookTokenAuthenticator{tokenReview, initialBackoff, implicitAuds}, nil } @@ -87,14 +89,14 @@ func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token // intersection in the response. // * otherwise return unauthenticated. wantAuds, checkAuds := authenticator.AudiencesFrom(ctx) - r := &authentication.TokenReview{ - Spec: authentication.TokenReviewSpec{ + r := &authenticationv1.TokenReview{ + Spec: authenticationv1.TokenReviewSpec{ Token: token, Audiences: wantAuds, }, } var ( - result *authentication.TokenReview + result *authenticationv1.TokenReview err error auds authenticator.Audiences ) @@ -150,32 +152,99 @@ func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token // tokenReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file, // and returns a TokenReviewInterface that uses that client. Note that the client submits TokenReview // requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted. -func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string) (authenticationclient.TokenReviewInterface, error) { +func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string, version string) (tokenReviewer, error) { localScheme := runtime.NewScheme() if err := scheme.AddToScheme(localScheme); err != nil { return nil, err } - if err := localScheme.SetVersionPriority(groupVersions...); err != nil { - return nil, err + + switch version { + case authenticationv1.SchemeGroupVersion.Version: + groupVersions := []schema.GroupVersion{authenticationv1.SchemeGroupVersion} + if err := localScheme.SetVersionPriority(groupVersions...); err != nil { + return nil, err + } + gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0) + if err != nil { + return nil, err + } + return &tokenReviewV1Client{gw}, nil + + case authenticationv1beta1.SchemeGroupVersion.Version: + groupVersions := []schema.GroupVersion{authenticationv1beta1.SchemeGroupVersion} + if err := localScheme.SetVersionPriority(groupVersions...); err != nil { + return nil, err + } + gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0) + if err != nil { + return nil, err + } + return &tokenReviewV1beta1Client{gw}, nil + + default: + return nil, fmt.Errorf( + "unsupported authentication webhook version %q, supported versions are %q, %q", + version, + authenticationv1.SchemeGroupVersion.Version, + authenticationv1beta1.SchemeGroupVersion.Version, + ) } - gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0) - if err != nil { - return nil, err - } - return &tokenReviewClient{gw}, nil } -type tokenReviewClient struct { +type tokenReviewV1Client struct { w *webhook.GenericWebhook } -func (t *tokenReviewClient) Create(tokenReview *authentication.TokenReview) (*authentication.TokenReview, error) { - return t.CreateContext(context.Background(), tokenReview) -} - -func (t *tokenReviewClient) CreateContext(ctx context.Context, tokenReview *authentication.TokenReview) (*authentication.TokenReview, error) { - result := &authentication.TokenReview{} - err := t.w.RestClient.Post().Context(ctx).Body(tokenReview).Do().Into(result) +func (t *tokenReviewV1Client) CreateContext(ctx context.Context, review *authenticationv1.TokenReview) (*authenticationv1.TokenReview, error) { + result := &authenticationv1.TokenReview{} + err := t.w.RestClient.Post().Context(ctx).Body(review).Do().Into(result) return result, err } + +type tokenReviewV1beta1Client struct { + w *webhook.GenericWebhook +} + +func (t *tokenReviewV1beta1Client) CreateContext(ctx context.Context, review *authenticationv1.TokenReview) (*authenticationv1.TokenReview, error) { + v1beta1Review := &authenticationv1beta1.TokenReview{Spec: v1SpecToV1beta1Spec(&review.Spec)} + v1beta1Result := &authenticationv1beta1.TokenReview{} + err := t.w.RestClient.Post().Context(ctx).Body(v1beta1Review).Do().Into(v1beta1Result) + if err != nil { + return nil, err + } + review.Status = v1beta1StatusToV1Status(&v1beta1Result.Status) + return review, nil +} + +func v1SpecToV1beta1Spec(in *authenticationv1.TokenReviewSpec) authenticationv1beta1.TokenReviewSpec { + return authenticationv1beta1.TokenReviewSpec{ + Token: in.Token, + Audiences: in.Audiences, + } +} + +func v1beta1StatusToV1Status(in *authenticationv1beta1.TokenReviewStatus) authenticationv1.TokenReviewStatus { + return authenticationv1.TokenReviewStatus{ + Authenticated: in.Authenticated, + User: v1beta1UserToV1User(in.User), + Audiences: in.Audiences, + Error: in.Error, + } +} + +func v1beta1UserToV1User(u authenticationv1beta1.UserInfo) authenticationv1.UserInfo { + var extra map[string]authenticationv1.ExtraValue + if u.Extra != nil { + extra = make(map[string]authenticationv1.ExtraValue, len(u.Extra)) + for k, v := range u.Extra { + extra[k] = authenticationv1.ExtraValue(v) + } + } + return authenticationv1.UserInfo{ + Username: u.Username, + UID: u.UID, + Groups: u.Groups, + Extra: extra, + } +} diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_test.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go similarity index 79% rename from staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_test.go rename to staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go index 8c8855a894d..518cf706301 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_test.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go @@ -31,26 +31,24 @@ import ( "testing" "time" - "k8s.io/api/authentication/v1beta1" + authenticationv1 "k8s.io/api/authentication/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/token/cache" "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/client-go/tools/clientcmd/api/v1" + v1 "k8s.io/client-go/tools/clientcmd/api/v1" ) -var apiAuds = authenticator.Audiences{"api"} - -// Service mocks a remote authentication service. -type Service interface { +// V1Service mocks a remote authentication service. +type V1Service interface { // Review looks at the TokenReviewSpec and provides an authentication // response in the TokenReviewStatus. - Review(*v1beta1.TokenReview) + Review(*authenticationv1.TokenReview) HTTPStatusCode() int } -// NewTestServer wraps a Service as an httptest.Server. -func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) { +// NewV1TestServer wraps a V1Service as an httptest.Server. +func NewV1TestServer(s V1Service, cert, key, caCert []byte) (*httptest.Server, error) { const webhookPath = "/testserver" var tlsConfig *tls.Config if cert != nil { @@ -81,14 +79,14 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error return } - var review v1beta1.TokenReview + var review authenticationv1.TokenReview bodyData, _ := ioutil.ReadAll(r.Body) if err := json.Unmarshal(bodyData, &review); err != nil { http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest) return } // ensure we received the serialized tokenreview as expected - if review.APIVersion != "authentication.k8s.io/v1beta1" { + if review.APIVersion != "authentication.k8s.io/v1" { http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest) return } @@ -124,7 +122,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error Status status `json:"status"` }{ Kind: "TokenReview", - APIVersion: v1beta1.SchemeGroupVersion.String(), + APIVersion: authenticationv1.SchemeGroupVersion.String(), Status: status{ review.Status.Authenticated, userInfo{ @@ -153,26 +151,26 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error } // A service that can be set to say yes or no to authentication requests. -type mockService struct { +type mockV1Service struct { allow bool statusCode int called int } -func (m *mockService) Review(r *v1beta1.TokenReview) { +func (m *mockV1Service) Review(r *authenticationv1.TokenReview) { m.called++ r.Status.Authenticated = m.allow if m.allow { r.Status.User.Username = "realHooman@email.com" } } -func (m *mockService) Allow() { m.allow = true } -func (m *mockService) Deny() { m.allow = false } -func (m *mockService) HTTPStatusCode() int { return m.statusCode } +func (m *mockV1Service) Allow() { m.allow = true } +func (m *mockV1Service) Deny() { m.allow = false } +func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode } -// newTokenAuthenticator creates a temporary kubeconfig file from the provided +// newV1TokenAuthenticator creates a temporary kubeconfig file from the provided // arguments and attempts to load a new WebhookTokenAuthenticator from it. -func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) { +func newV1TokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) { tempfile, err := ioutil.TempFile("", "") if err != nil { return nil, err @@ -195,7 +193,7 @@ func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, c return nil, err } - c, err := tokenReviewInterfaceFromKubeconfig(p) + c, err := tokenReviewInterfaceFromKubeconfig(p, "v1") if err != nil { return nil, err } @@ -208,7 +206,7 @@ func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, c return cache.New(authn, false, cacheTime, cacheTime), nil } -func TestTLSConfig(t *testing.T) { +func TestV1TLSConfig(t *testing.T) { tests := []struct { test string clientCert, clientKey, clientCA []byte @@ -251,17 +249,17 @@ func TestTLSConfig(t *testing.T) { for _, tt := range tests { // Use a closure so defer statements trigger between loop iterations. func() { - service := new(mockService) + service := new(mockV1Service) service.statusCode = 200 - server, err := NewTestServer(service, tt.serverCert, tt.serverKey, tt.serverCA) + server, err := NewV1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA) if err != nil { t.Errorf("%s: failed to create server: %v", tt.test, err) return } defer server.Close() - wh, err := newTokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil) + wh, err := newV1TokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil) if err != nil { t.Errorf("%s: failed to create client: %v", tt.test, err) return @@ -293,47 +291,47 @@ func TestTLSConfig(t *testing.T) { } } -// recorderService records all token review requests, and responds with the +// recorderV1Service records all token review requests, and responds with the // provided TokenReviewStatus. -type recorderService struct { - lastRequest v1beta1.TokenReview - response v1beta1.TokenReviewStatus +type recorderV1Service struct { + lastRequest authenticationv1.TokenReview + response authenticationv1.TokenReviewStatus } -func (rec *recorderService) Review(r *v1beta1.TokenReview) { +func (rec *recorderV1Service) Review(r *authenticationv1.TokenReview) { rec.lastRequest = *r r.Status = rec.response } -func (rec *recorderService) HTTPStatusCode() int { return 200 } +func (rec *recorderV1Service) HTTPStatusCode() int { return 200 } -func TestWebhookTokenAuthenticator(t *testing.T) { - serv := &recorderService{} +func TestV1WebhookTokenAuthenticator(t *testing.T) { + serv := &recorderV1Service{} - s, err := NewTestServer(serv, serverCert, serverKey, caCert) + s, err := NewV1TestServer(serv, serverCert, serverKey, caCert) if err != nil { t.Fatal(err) } defer s.Close() expTypeMeta := metav1.TypeMeta{ - APIVersion: "authentication.k8s.io/v1beta1", + APIVersion: "authentication.k8s.io/v1", Kind: "TokenReview", } tests := []struct { description string implicitAuds, reqAuds authenticator.Audiences - serverResponse v1beta1.TokenReviewStatus + serverResponse authenticationv1.TokenReviewStatus expectedAuthenticated bool expectedUser *user.DefaultInfo expectedAuds authenticator.Audiences }{ { description: "successful response should pass through all user info.", - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: true, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "somebody", }, }, @@ -344,13 +342,13 @@ func TestWebhookTokenAuthenticator(t *testing.T) { }, { description: "successful response should pass through all user info.", - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: true, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "person@place.com", UID: "abcd-1234", Groups: []string{"stuff-dev", "main-eng"}, - Extra: map[string]v1beta1.ExtraValue{"foo": {"bar", "baz"}}, + Extra: map[string]authenticationv1.ExtraValue{"foo": {"bar", "baz"}}, }, }, expectedAuthenticated: true, @@ -363,9 +361,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) { }, { description: "unauthenticated shouldn't even include extra provided info.", - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: false, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "garbage", UID: "abcd-1234", Groups: []string{"not-actually-used"}, @@ -376,7 +374,7 @@ func TestWebhookTokenAuthenticator(t *testing.T) { }, { description: "unauthenticated shouldn't even include extra provided info.", - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: false, }, expectedAuthenticated: false, @@ -386,9 +384,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) { description: "good audience", implicitAuds: apiAuds, reqAuds: apiAuds, - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: true, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "somebody", }, }, @@ -402,9 +400,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) { description: "good audience", implicitAuds: append(apiAuds, "other"), reqAuds: apiAuds, - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: true, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "somebody", }, }, @@ -418,7 +416,7 @@ func TestWebhookTokenAuthenticator(t *testing.T) { description: "bad audiences", implicitAuds: apiAuds, reqAuds: authenticator.Audiences{"other"}, - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: false, }, expectedAuthenticated: false, @@ -428,9 +426,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) { implicitAuds: apiAuds, reqAuds: authenticator.Audiences{"other"}, // webhook authenticator hasn't been upgraded to support audience. - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: true, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "somebody", }, }, @@ -440,9 +438,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) { description: "audience aware backend", implicitAuds: apiAuds, reqAuds: apiAuds, - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: true, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "somebody", }, Audiences: []string(apiAuds), @@ -455,9 +453,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) { }, { description: "audience aware backend", - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: true, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "somebody", }, Audiences: []string(apiAuds), @@ -471,9 +469,9 @@ func TestWebhookTokenAuthenticator(t *testing.T) { description: "audience aware backend", implicitAuds: apiAuds, reqAuds: apiAuds, - serverResponse: v1beta1.TokenReviewStatus{ + serverResponse: authenticationv1.TokenReviewStatus{ Authenticated: true, - User: v1beta1.UserInfo{ + User: authenticationv1.UserInfo{ Username: "somebody", }, Audiences: []string{"other"}, @@ -484,7 +482,7 @@ func TestWebhookTokenAuthenticator(t *testing.T) { token := "my-s3cr3t-t0ken" for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - wh, err := newTokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds) + wh, err := newV1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds) if err != nil { t.Fatal(err) } @@ -523,13 +521,13 @@ func TestWebhookTokenAuthenticator(t *testing.T) { } } -type authenticationUserInfo v1beta1.UserInfo +type authenticationV1UserInfo authenticationv1.UserInfo -func (a *authenticationUserInfo) GetName() string { return a.Username } -func (a *authenticationUserInfo) GetUID() string { return a.UID } -func (a *authenticationUserInfo) GetGroups() []string { return a.Groups } +func (a *authenticationV1UserInfo) GetName() string { return a.Username } +func (a *authenticationV1UserInfo) GetUID() string { return a.UID } +func (a *authenticationV1UserInfo) GetGroups() []string { return a.Groups } -func (a *authenticationUserInfo) GetExtra() map[string][]string { +func (a *authenticationV1UserInfo) GetExtra() map[string][]string { if a.Extra == nil { return nil } @@ -541,23 +539,23 @@ func (a *authenticationUserInfo) GetExtra() map[string][]string { return ret } -// Ensure v1beta1.UserInfo contains the fields necessary to implement the +// Ensure authenticationv1.UserInfo contains the fields necessary to implement the // user.Info interface. -var _ user.Info = (*authenticationUserInfo)(nil) +var _ user.Info = (*authenticationV1UserInfo)(nil) // TestWebhookCache verifies that error responses from the server are not // cached, but successful responses are. It also ensures that the webhook // call is retried on 429 and 500+ errors -func TestWebhookCacheAndRetry(t *testing.T) { - serv := new(mockService) - s, err := NewTestServer(serv, serverCert, serverKey, caCert) +func TestV1WebhookCacheAndRetry(t *testing.T) { + serv := new(mockV1Service) + s, err := NewV1TestServer(serv, serverCert, serverKey, caCert) if err != nil { t.Fatal(err) } defer s.Close() // Create an authenticator that caches successful responses "forever" (100 days). - wh, err := newTokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil) + wh, err := newV1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil) if err != nil { t.Fatal(err) } diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go new file mode 100644 index 00000000000..5c228b69025 --- /dev/null +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go @@ -0,0 +1,686 @@ +/* +Copyright 2016 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 webhook + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "reflect" + "testing" + "time" + + authenticationv1beta1 "k8s.io/api/authentication/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/token/cache" + "k8s.io/apiserver/pkg/authentication/user" + v1 "k8s.io/client-go/tools/clientcmd/api/v1" +) + +var apiAuds = authenticator.Audiences{"api"} + +// V1beta1Service mocks a remote authentication service. +type V1beta1Service interface { + // Review looks at the TokenReviewSpec and provides an authentication + // response in the TokenReviewStatus. + Review(*authenticationv1beta1.TokenReview) + HTTPStatusCode() int +} + +// NewV1beta1TestServer wraps a V1beta1Service as an httptest.Server. +func NewV1beta1TestServer(s V1beta1Service, cert, key, caCert []byte) (*httptest.Server, error) { + const webhookPath = "/testserver" + var tlsConfig *tls.Config + if cert != nil { + cert, err := tls.X509KeyPair(cert, key) + if err != nil { + return nil, err + } + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + + if caCert != nil { + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(caCert) + if tlsConfig == nil { + tlsConfig = &tls.Config{} + } + tlsConfig.ClientCAs = rootCAs + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + + serveHTTP := func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed) + return + } + if r.URL.Path != webhookPath { + http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound) + return + } + + var review authenticationv1beta1.TokenReview + bodyData, _ := ioutil.ReadAll(r.Body) + if err := json.Unmarshal(bodyData, &review); err != nil { + http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest) + return + } + // ensure we received the serialized tokenreview as expected + if review.APIVersion != "authentication.k8s.io/v1beta1" { + http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest) + return + } + // once we have a successful request, always call the review to record that we were called + s.Review(&review) + if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 { + http.Error(w, "HTTP Error", s.HTTPStatusCode()) + return + } + type userInfo struct { + Username string `json:"username"` + UID string `json:"uid"` + Groups []string `json:"groups"` + Extra map[string][]string `json:"extra"` + } + type status struct { + Authenticated bool `json:"authenticated"` + User userInfo `json:"user"` + Audiences []string `json:"audiences"` + } + + var extra map[string][]string + if review.Status.User.Extra != nil { + extra = map[string][]string{} + for k, v := range review.Status.User.Extra { + extra[k] = v + } + } + + resp := struct { + Kind string `json:"kind"` + APIVersion string `json:"apiVersion"` + Status status `json:"status"` + }{ + Kind: "TokenReview", + APIVersion: authenticationv1beta1.SchemeGroupVersion.String(), + Status: status{ + review.Status.Authenticated, + userInfo{ + Username: review.Status.User.Username, + UID: review.Status.User.UID, + Groups: review.Status.User.Groups, + Extra: extra, + }, + review.Status.Audiences, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + } + + server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP)) + server.TLS = tlsConfig + server.StartTLS() + + // Adjust the path to point to our custom path + serverURL, _ := url.Parse(server.URL) + serverURL.Path = webhookPath + server.URL = serverURL.String() + + return server, nil +} + +// A service that can be set to say yes or no to authentication requests. +type mockV1beta1Service struct { + allow bool + statusCode int + called int +} + +func (m *mockV1beta1Service) Review(r *authenticationv1beta1.TokenReview) { + m.called++ + r.Status.Authenticated = m.allow + if m.allow { + r.Status.User.Username = "realHooman@email.com" + } +} +func (m *mockV1beta1Service) Allow() { m.allow = true } +func (m *mockV1beta1Service) Deny() { m.allow = false } +func (m *mockV1beta1Service) HTTPStatusCode() int { return m.statusCode } + +// newV1beta1TokenAuthenticator creates a temporary kubeconfig file from the provided +// arguments and attempts to load a new WebhookTokenAuthenticator from it. +func newV1beta1TokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) { + tempfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, err + } + p := tempfile.Name() + defer os.Remove(p) + config := v1.Config{ + Clusters: []v1.NamedCluster{ + { + Cluster: v1.Cluster{Server: serverURL, CertificateAuthorityData: ca}, + }, + }, + AuthInfos: []v1.NamedAuthInfo{ + { + AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey}, + }, + }, + } + if err := json.NewEncoder(tempfile).Encode(config); err != nil { + return nil, err + } + + c, err := tokenReviewInterfaceFromKubeconfig(p, "v1beta1") + if err != nil { + return nil, err + } + + authn, err := newWithBackoff(c, 0, implicitAuds) + if err != nil { + return nil, err + } + + return cache.New(authn, false, cacheTime, cacheTime), nil +} + +func TestV1beta1TLSConfig(t *testing.T) { + tests := []struct { + test string + clientCert, clientKey, clientCA []byte + serverCert, serverKey, serverCA []byte + wantErr bool + }{ + { + test: "TLS setup between client and server", + clientCert: clientCert, clientKey: clientKey, clientCA: caCert, + serverCert: serverCert, serverKey: serverKey, serverCA: caCert, + }, + { + test: "Server does not require client auth", + clientCA: caCert, + serverCert: serverCert, serverKey: serverKey, + }, + { + test: "Server does not require client auth, client provides it", + clientCert: clientCert, clientKey: clientKey, clientCA: caCert, + serverCert: serverCert, serverKey: serverKey, + }, + { + test: "Client does not trust server", + clientCert: clientCert, clientKey: clientKey, + serverCert: serverCert, serverKey: serverKey, + wantErr: true, + }, + { + test: "Server does not trust client", + clientCert: clientCert, clientKey: clientKey, clientCA: caCert, + serverCert: serverCert, serverKey: serverKey, serverCA: badCACert, + wantErr: true, + }, + { + // Plugin does not support insecure configurations. + test: "Server is using insecure connection", + wantErr: true, + }, + } + for _, tt := range tests { + // Use a closure so defer statements trigger between loop iterations. + func() { + service := new(mockV1beta1Service) + service.statusCode = 200 + + server, err := NewV1beta1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA) + if err != nil { + t.Errorf("%s: failed to create server: %v", tt.test, err) + return + } + defer server.Close() + + wh, err := newV1beta1TokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil) + if err != nil { + t.Errorf("%s: failed to create client: %v", tt.test, err) + return + } + + // Allow all and see if we get an error. + service.Allow() + _, authenticated, err := wh.AuthenticateToken(context.Background(), "t0k3n") + if tt.wantErr { + if err == nil { + t.Errorf("expected error making authorization request: %v", err) + } + return + } + if !authenticated { + t.Errorf("%s: failed to authenticate token", tt.test) + return + } + + service.Deny() + _, authenticated, err = wh.AuthenticateToken(context.Background(), "t0k3n") + if err != nil { + t.Errorf("%s: unexpectedly failed AuthenticateToken", tt.test) + } + if authenticated { + t.Errorf("%s: incorrectly authenticated token", tt.test) + } + }() + } +} + +// recorderV1beta1Service records all token review requests, and responds with the +// provided TokenReviewStatus. +type recorderV1beta1Service struct { + lastRequest authenticationv1beta1.TokenReview + response authenticationv1beta1.TokenReviewStatus +} + +func (rec *recorderV1beta1Service) Review(r *authenticationv1beta1.TokenReview) { + rec.lastRequest = *r + r.Status = rec.response +} + +func (rec *recorderV1beta1Service) HTTPStatusCode() int { return 200 } + +func TestV1beta1WebhookTokenAuthenticator(t *testing.T) { + serv := &recorderV1beta1Service{} + + s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert) + if err != nil { + t.Fatal(err) + } + defer s.Close() + + expTypeMeta := metav1.TypeMeta{ + APIVersion: "authentication.k8s.io/v1beta1", + Kind: "TokenReview", + } + + tests := []struct { + description string + implicitAuds, reqAuds authenticator.Audiences + serverResponse authenticationv1beta1.TokenReviewStatus + expectedAuthenticated bool + expectedUser *user.DefaultInfo + expectedAuds authenticator.Audiences + }{ + { + description: "successful response should pass through all user info.", + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: true, + User: authenticationv1beta1.UserInfo{ + Username: "somebody", + }, + }, + expectedAuthenticated: true, + expectedUser: &user.DefaultInfo{ + Name: "somebody", + }, + }, + { + description: "successful response should pass through all user info.", + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: true, + User: authenticationv1beta1.UserInfo{ + Username: "person@place.com", + UID: "abcd-1234", + Groups: []string{"stuff-dev", "main-eng"}, + Extra: map[string]authenticationv1beta1.ExtraValue{"foo": {"bar", "baz"}}, + }, + }, + expectedAuthenticated: true, + expectedUser: &user.DefaultInfo{ + Name: "person@place.com", + UID: "abcd-1234", + Groups: []string{"stuff-dev", "main-eng"}, + Extra: map[string][]string{"foo": {"bar", "baz"}}, + }, + }, + { + description: "unauthenticated shouldn't even include extra provided info.", + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: false, + User: authenticationv1beta1.UserInfo{ + Username: "garbage", + UID: "abcd-1234", + Groups: []string{"not-actually-used"}, + }, + }, + expectedAuthenticated: false, + expectedUser: nil, + }, + { + description: "unauthenticated shouldn't even include extra provided info.", + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: false, + }, + expectedAuthenticated: false, + expectedUser: nil, + }, + { + description: "good audience", + implicitAuds: apiAuds, + reqAuds: apiAuds, + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: true, + User: authenticationv1beta1.UserInfo{ + Username: "somebody", + }, + }, + expectedAuthenticated: true, + expectedUser: &user.DefaultInfo{ + Name: "somebody", + }, + expectedAuds: apiAuds, + }, + { + description: "good audience", + implicitAuds: append(apiAuds, "other"), + reqAuds: apiAuds, + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: true, + User: authenticationv1beta1.UserInfo{ + Username: "somebody", + }, + }, + expectedAuthenticated: true, + expectedUser: &user.DefaultInfo{ + Name: "somebody", + }, + expectedAuds: apiAuds, + }, + { + description: "bad audiences", + implicitAuds: apiAuds, + reqAuds: authenticator.Audiences{"other"}, + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: false, + }, + expectedAuthenticated: false, + }, + { + description: "bad audiences", + implicitAuds: apiAuds, + reqAuds: authenticator.Audiences{"other"}, + // webhook authenticator hasn't been upgraded to support audience. + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: true, + User: authenticationv1beta1.UserInfo{ + Username: "somebody", + }, + }, + expectedAuthenticated: false, + }, + { + description: "audience aware backend", + implicitAuds: apiAuds, + reqAuds: apiAuds, + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: true, + User: authenticationv1beta1.UserInfo{ + Username: "somebody", + }, + Audiences: []string(apiAuds), + }, + expectedAuthenticated: true, + expectedUser: &user.DefaultInfo{ + Name: "somebody", + }, + expectedAuds: apiAuds, + }, + { + description: "audience aware backend", + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: true, + User: authenticationv1beta1.UserInfo{ + Username: "somebody", + }, + Audiences: []string(apiAuds), + }, + expectedAuthenticated: true, + expectedUser: &user.DefaultInfo{ + Name: "somebody", + }, + }, + { + description: "audience aware backend", + implicitAuds: apiAuds, + reqAuds: apiAuds, + serverResponse: authenticationv1beta1.TokenReviewStatus{ + Authenticated: true, + User: authenticationv1beta1.UserInfo{ + Username: "somebody", + }, + Audiences: []string{"other"}, + }, + expectedAuthenticated: false, + }, + } + token := "my-s3cr3t-t0ken" + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + wh, err := newV1beta1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + if tt.reqAuds != nil { + ctx = authenticator.WithAudiences(ctx, tt.reqAuds) + } + + serv.response = tt.serverResponse + resp, authenticated, err := wh.AuthenticateToken(ctx, token) + if err != nil { + t.Fatalf("authentication failed: %v", err) + } + if serv.lastRequest.Spec.Token != token { + t.Errorf("Server did not see correct token. Got %q, expected %q.", + serv.lastRequest.Spec.Token, token) + } + if !reflect.DeepEqual(serv.lastRequest.TypeMeta, expTypeMeta) { + t.Errorf("Server did not see correct TypeMeta. Got %v, expected %v", + serv.lastRequest.TypeMeta, expTypeMeta) + } + if authenticated != tt.expectedAuthenticated { + t.Errorf("Plugin returned incorrect authentication response. Got %t, expected %t.", + authenticated, tt.expectedAuthenticated) + } + if resp != nil && tt.expectedUser != nil && !reflect.DeepEqual(resp.User, tt.expectedUser) { + t.Errorf("Plugin returned incorrect user. Got %#v, expected %#v", + resp.User, tt.expectedUser) + } + if resp != nil && tt.expectedAuds != nil && !reflect.DeepEqual(resp.Audiences, tt.expectedAuds) { + t.Errorf("Plugin returned incorrect audiences. Got %#v, expected %#v", + resp.Audiences, tt.expectedAuds) + } + }) + } +} + +type authenticationV1beta1UserInfo authenticationv1beta1.UserInfo + +func (a *authenticationV1beta1UserInfo) GetName() string { return a.Username } +func (a *authenticationV1beta1UserInfo) GetUID() string { return a.UID } +func (a *authenticationV1beta1UserInfo) GetGroups() []string { return a.Groups } + +func (a *authenticationV1beta1UserInfo) GetExtra() map[string][]string { + if a.Extra == nil { + return nil + } + ret := map[string][]string{} + for k, v := range a.Extra { + ret[k] = []string(v) + } + + return ret +} + +// Ensure authenticationv1beta1.UserInfo contains the fields necessary to implement the +// user.Info interface. +var _ user.Info = (*authenticationV1beta1UserInfo)(nil) + +// TestWebhookCache verifies that error responses from the server are not +// cached, but successful responses are. It also ensures that the webhook +// call is retried on 429 and 500+ errors +func TestV1beta1WebhookCacheAndRetry(t *testing.T) { + serv := new(mockV1beta1Service) + s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert) + if err != nil { + t.Fatal(err) + } + defer s.Close() + + // Create an authenticator that caches successful responses "forever" (100 days). + wh, err := newV1beta1TokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil) + if err != nil { + t.Fatal(err) + } + + testcases := []struct { + description string + + token string + allow bool + code int + + expectError bool + expectOk bool + expectCalls int + }{ + { + description: "t0k3n, 500 error, retries and fails", + + token: "t0k3n", + allow: false, + code: 500, + + expectError: true, + expectOk: false, + expectCalls: 5, + }, + { + description: "t0k3n, 404 error, fails (but no retry)", + + token: "t0k3n", + allow: false, + code: 404, + + expectError: true, + expectOk: false, + expectCalls: 1, + }, + { + description: "t0k3n, 200 response, allowed, succeeds with a single call", + + token: "t0k3n", + allow: true, + code: 200, + + expectError: false, + expectOk: true, + expectCalls: 1, + }, + { + description: "t0k3n, 500 response, disallowed, but never called because previous 200 response was cached", + + token: "t0k3n", + allow: false, + code: 500, + + expectError: false, + expectOk: true, + expectCalls: 0, + }, + + { + description: "an0th3r_t0k3n, 500 response, disallowed, should be called again with retries", + + token: "an0th3r_t0k3n", + allow: false, + code: 500, + + expectError: true, + expectOk: false, + expectCalls: 5, + }, + { + description: "an0th3r_t0k3n, 429 response, disallowed, should be called again with retries", + + token: "an0th3r_t0k3n", + allow: false, + code: 429, + + expectError: true, + expectOk: false, + expectCalls: 5, + }, + { + description: "an0th3r_t0k3n, 200 response, allowed, succeeds with a single call", + + token: "an0th3r_t0k3n", + allow: true, + code: 200, + + expectError: false, + expectOk: true, + expectCalls: 1, + }, + { + description: "an0th3r_t0k3n, 500 response, disallowed, but never called because previous 200 response was cached", + + token: "an0th3r_t0k3n", + allow: false, + code: 500, + + expectError: false, + expectOk: true, + expectCalls: 0, + }, + } + + for _, testcase := range testcases { + t.Run(testcase.description, func(t *testing.T) { + serv.allow = testcase.allow + serv.statusCode = testcase.code + serv.called = 0 + + _, ok, err := wh.AuthenticateToken(context.Background(), testcase.token) + hasError := err != nil + if hasError != testcase.expectError { + t.Errorf("Webhook returned HTTP %d, expected error=%v, but got error %v", testcase.code, testcase.expectError, err) + } + if serv.called != testcase.expectCalls { + t.Errorf("Expected %d calls, got %d", testcase.expectCalls, serv.called) + } + if ok != testcase.expectOk { + t.Errorf("Expected ok=%v, got %v", testcase.expectOk, ok) + } + }) + } +} diff --git a/test/integration/auth/auth_test.go b/test/integration/auth/auth_test.go index 8e87014a406..852c2228b69 100644 --- a/test/integration/auth/auth_test.go +++ b/test/integration/auth/auth_test.go @@ -86,7 +86,7 @@ func getTestWebhookTokenAuth(serverURL string) (authenticator.Request, error) { if err := json.NewEncoder(kubecfgFile).Encode(config); err != nil { return nil, err } - webhookTokenAuth, err := webhook.New(kubecfgFile.Name(), nil) + webhookTokenAuth, err := webhook.New(kubecfgFile.Name(), "v1beta1", nil) if err != nil { return nil, err }