Switch kubelet/aggregated API servers to use v1 tokenreviews

This commit is contained in:
Jordan Liggitt 2019-11-04 22:41:32 -05:00
parent 0afc8423f8
commit 5ef4fe959a
11 changed files with 958 additions and 108 deletions

View File

@ -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{})))
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -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
}