mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 10:51:29 +00:00
Switch kubelet/aggregated API servers to use v1 tokenreviews
This commit is contained in:
parent
0afc8423f8
commit
5ef4fe959a
@ -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{})))
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user