Merge pull request #84768 from liggitt/delegated-authnz-v1

switch delegated authnz to v1 APIs
This commit is contained in:
Kubernetes Prow Robot 2019-11-12 04:48:35 -08:00 committed by GitHub
commit 681d22428b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1928 additions and 207 deletions

View File

@ -53,7 +53,7 @@ go_test(
"//pkg/kubeapiserver/options:go_default_library",
"//pkg/kubelet/client:go_default_library",
"//pkg/master/reconcilers:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
@ -64,6 +64,8 @@ go_test(
"//staging/src/k8s.io/component-base/cli/flag:go_default_library",
"//staging/src/k8s.io/component-base/cli/globalflag:go_default_library",
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
"//vendor/github.com/google/go-cmp/cmp/cmpopts:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
],
)

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{
@ -287,6 +290,7 @@ func TestAddFlags(t *testing.T) {
WebhookConfigFile: "/webhook-config",
WebhookCacheAuthorizedTTL: 180000000000,
WebhookCacheUnauthorizedTTL: 60000000000,
WebhookVersion: "v1beta1",
},
CloudProvider: &kubeoptions.CloudProviderOptions{
CloudConfigFile: "/cloud-config",
@ -305,6 +309,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

@ -125,8 +125,8 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/server/healthz:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
"//staging/src/k8s.io/client-go/rest:go_default_library",

View File

@ -28,8 +28,8 @@ 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"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
"k8s.io/kubernetes/pkg/kubelet/server"
@ -43,8 +43,8 @@ func BuildAuth(nodeName types.NodeName, client clientset.Interface, config kubel
sarClient authorizationclient.SubjectAccessReviewInterface
)
if client != nil && !reflect.ValueOf(client).IsNil() {
tokenClient = client.AuthenticationV1beta1().TokenReviews()
sarClient = client.AuthorizationV1beta1().SubjectAccessReviews()
tokenClient = client.AuthenticationV1().TokenReviews()
sarClient = client.AuthorizationV1().SubjectAccessReviews()
}
authenticator, err := BuildAuthn(tokenClient, config.Authentication)

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

@ -46,6 +46,8 @@ type Config struct {
// Kubeconfig file for Webhook authorization plugin.
WebhookConfigFile string
// API version of subject access reviews to send to the webhook (e.g. "v1", "v1beta1")
WebhookVersion string
// TTL for caching of authorized responses from the webhook server.
WebhookCacheAuthorizedTTL time.Duration
// TTL for caching of unauthorized responses from the webhook server.
@ -98,6 +100,7 @@ func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, erro
ruleResolvers = append(ruleResolvers, abacAuthorizer)
case modes.ModeWebhook:
webhookAuthorizer, err := webhook.New(config.WebhookConfigFile,
config.WebhookVersion,
config.WebhookCacheAuthorizedTTL,
config.WebhookCacheUnauthorizedTTL)
if err != nil {

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

@ -33,6 +33,7 @@ type BuiltInAuthorizationOptions struct {
Modes []string
PolicyFile string
WebhookConfigFile string
WebhookVersion string
WebhookCacheAuthorizedTTL time.Duration
WebhookCacheUnauthorizedTTL time.Duration
}
@ -40,6 +41,7 @@ type BuiltInAuthorizationOptions struct {
func NewBuiltInAuthorizationOptions() *BuiltInAuthorizationOptions {
return &BuiltInAuthorizationOptions{
Modes: []string{authzmodes.ModeAlwaysAllow},
WebhookVersion: "v1beta1",
WebhookCacheAuthorizedTTL: 5 * time.Minute,
WebhookCacheUnauthorizedTTL: 30 * time.Second,
}
@ -99,6 +101,9 @@ func (s *BuiltInAuthorizationOptions) AddFlags(fs *pflag.FlagSet) {
"File with webhook configuration in kubeconfig format, used with --authorization-mode=Webhook. "+
"The API server will query the remote service to determine access on the API server's secure port.")
fs.StringVar(&s.WebhookVersion, "authorization-webhook-version", s.WebhookVersion, ""+
"The API version of the authorization.k8s.io SubjectAccessReview to send to and expect from the webhook.")
fs.DurationVar(&s.WebhookCacheAuthorizedTTL, "authorization-webhook-cache-authorized-ttl",
s.WebhookCacheAuthorizedTTL,
"The duration to cache 'authorized' responses from the webhook authorizer.")

View File

@ -27,7 +27,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
],
)

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

@ -28,7 +28,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
"//staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
],
)

View File

@ -21,7 +21,7 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
)
// DelegatingAuthorizerConfig 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

@ -146,7 +146,7 @@ func (s *DelegatingAuthorizationOptions) toAuthorizer(client kubernetes.Interfac
klog.Warningf("No authorization-kubeconfig provided, so SubjectAccessReview of authorization tokens won't work.")
} else {
cfg := authorizerfactory.DelegatingAuthorizerConfig{
SubjectAccessReviewClient: client.AuthorizationV1beta1().SubjectAccessReviews(),
SubjectAccessReviewClient: client.AuthorizationV1().SubjectAccessReviews(),
AllowCacheTTL: s.AllowCacheTTL,
DenyCacheTTL: s.DenyCacheTTL,
}

View File

@ -10,16 +10,21 @@ go_test(
name = "go_default_test",
srcs = [
"certs_test.go",
"webhook_test.go",
"round_trip_test.go",
"webhook_v1_test.go",
"webhook_v1beta1_test.go",
],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/api/authentication/v1:go_default_library",
"//staging/src/k8s.io/api/authentication/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/token/cache:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library",
],
)
@ -29,6 +34,7 @@ go_library(
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook",
importpath = "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook",
deps = [
"//staging/src/k8s.io/api/authentication/v1:go_default_library",
"//staging/src/k8s.io/api/authentication/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
@ -36,7 +42,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
)

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

@ -10,16 +10,20 @@ go_test(
name = "go_default_test",
srcs = [
"certs_test.go",
"webhook_test.go",
"round_trip_test.go",
"webhook_v1_test.go",
"webhook_v1beta1_test.go",
],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
"//staging/src/k8s.io/api/authorization/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library",
],
)
@ -29,6 +33,7 @@ go_library(
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook",
importpath = "k8s.io/apiserver/plugin/pkg/authorizer/webhook",
deps = [
"//staging/src/k8s.io/api/authorization/v1:go_default_library",
"//staging/src/k8s.io/api/authorization/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
@ -37,7 +42,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
)

View File

@ -0,0 +1,113 @@
/*
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"
authorizationv1 "k8s.io/api/authorization/v1"
authorizationv1beta1 "k8s.io/api/authorization/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 := &authorizationv1.SubjectAccessReview{}
f.Fuzz(&original.Spec)
f.Fuzz(&original.Status)
converted := &authorizationv1beta1.SubjectAccessReview{
Spec: v1SpecToV1beta1Spec(&original.Spec),
Status: v1StatusToV1beta1Status(original.Status),
}
roundtripped := &authorizationv1.SubjectAccessReview{
Spec: v1beta1SpecToV1Spec(converted.Spec),
Status: v1beta1StatusToV1Status(&converted.Status),
}
if !reflect.DeepEqual(original, roundtripped) {
t.Errorf("diff %s", diff.ObjectReflectDiff(original, roundtripped))
}
}
}
// v1StatusToV1beta1Status is only needed to verify round-trip fidelity
func v1StatusToV1beta1Status(in authorizationv1.SubjectAccessReviewStatus) authorizationv1beta1.SubjectAccessReviewStatus {
return authorizationv1beta1.SubjectAccessReviewStatus{
Allowed: in.Allowed,
Denied: in.Denied,
Reason: in.Reason,
EvaluationError: in.EvaluationError,
}
}
// v1beta1SpecToV1Spec is only needed to verify round-trip fidelity
func v1beta1SpecToV1Spec(in authorizationv1beta1.SubjectAccessReviewSpec) authorizationv1.SubjectAccessReviewSpec {
return authorizationv1.SubjectAccessReviewSpec{
ResourceAttributes: v1beta1ResourceAttributesToV1ResourceAttributes(in.ResourceAttributes),
NonResourceAttributes: v1beta1NonResourceAttributesToV1NonResourceAttributes(in.NonResourceAttributes),
User: in.User,
Groups: in.Groups,
Extra: v1beta1ExtraToV1Extra(in.Extra),
UID: in.UID,
}
}
func v1beta1ResourceAttributesToV1ResourceAttributes(in *authorizationv1beta1.ResourceAttributes) *authorizationv1.ResourceAttributes {
if in == nil {
return nil
}
return &authorizationv1.ResourceAttributes{
Namespace: in.Namespace,
Verb: in.Verb,
Group: in.Group,
Version: in.Version,
Resource: in.Resource,
Subresource: in.Subresource,
Name: in.Name,
}
}
func v1beta1NonResourceAttributesToV1NonResourceAttributes(in *authorizationv1beta1.NonResourceAttributes) *authorizationv1.NonResourceAttributes {
if in == nil {
return nil
}
return &authorizationv1.NonResourceAttributes{
Path: in.Path,
Verb: in.Verb,
}
}
func v1beta1ExtraToV1Extra(in map[string]authorizationv1beta1.ExtraValue) map[string]authorizationv1.ExtraValue {
if in == nil {
return nil
}
ret := make(map[string]authorizationv1.ExtraValue, len(in))
for k, v := range in {
ret[k] = authorizationv1.ExtraValue(v)
}
return ret
}

View File

@ -25,7 +25,8 @@ import (
"k8s.io/klog"
authorization "k8s.io/api/authorization/v1beta1"
authorizationv1 "k8s.io/api/authorization/v1"
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/cache"
@ -33,11 +34,7 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/kubernetes/scheme"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
)
var (
groupVersions = []schema.GroupVersion{authorization.SchemeGroupVersion}
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
)
const (
@ -49,8 +46,12 @@ const (
// Ensure Webhook implements the authorizer.Authorizer interface.
var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil)
type subjectAccessReviewer interface {
CreateContext(context.Context, *authorizationv1.SubjectAccessReview) (*authorizationv1.SubjectAccessReview, error)
}
type WebhookAuthorizer struct {
subjectAccessReview authorizationclient.SubjectAccessReviewInterface
subjectAccessReview subjectAccessReviewer
responseCache *cache.LRUExpireCache
authorizedTTL time.Duration
unauthorizedTTL time.Duration
@ -59,12 +60,11 @@ type WebhookAuthorizer struct {
}
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
func NewFromInterface(subjectAccessReview authorizationclient.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
func NewFromInterface(subjectAccessReview authorizationv1client.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff)
}
// New creates a new WebhookAuthorizer from the provided kubeconfig file.
//
// The config's cluster field is used to refer to the remote service, user refers to the returned authorizer.
//
// # clusters refers to the remote service.
@ -83,8 +83,8 @@ func NewFromInterface(subjectAccessReview authorizationclient.SubjectAccessRevie
//
// For additional HTTP configuration, refer to the kubeconfig documentation
// https://kubernetes.io/docs/user-guide/kubeconfig-file/.
func New(kubeConfigFile string, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
subjectAccessReview, err := subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile)
func New(kubeConfigFile string, version string, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
subjectAccessReview, err := subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile, version)
if err != nil {
return nil, err
}
@ -92,7 +92,7 @@ func New(kubeConfigFile string, authorizedTTL, unauthorizedTTL time.Duration) (*
}
// newWithBackoff allows tests to skip the sleep.
func newWithBackoff(subjectAccessReview authorizationclient.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL, initialBackoff time.Duration) (*WebhookAuthorizer, error) {
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL, initialBackoff time.Duration) (*WebhookAuthorizer, error) {
return &WebhookAuthorizer{
subjectAccessReview: subjectAccessReview,
responseCache: cache.NewLRUExpireCache(1024),
@ -151,9 +151,9 @@ func newWithBackoff(subjectAccessReview authorizationclient.SubjectAccessReviewI
// encounter an error. We are failing open now to preserve backwards compatible
// behavior.
func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
r := &authorization.SubjectAccessReview{}
r := &authorizationv1.SubjectAccessReview{}
if user := attr.GetUser(); user != nil {
r.Spec = authorization.SubjectAccessReviewSpec{
r.Spec = authorizationv1.SubjectAccessReviewSpec{
User: user.GetName(),
UID: user.GetUID(),
Groups: user.GetGroups(),
@ -162,7 +162,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
}
if attr.IsResourceRequest() {
r.Spec.ResourceAttributes = &authorization.ResourceAttributes{
r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{
Namespace: attr.GetNamespace(),
Verb: attr.GetVerb(),
Group: attr.GetAPIGroup(),
@ -172,7 +172,7 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
Name: attr.GetName(),
}
} else {
r.Spec.NonResourceAttributes = &authorization.NonResourceAttributes{
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
Path: attr.GetPath(),
Verb: attr.GetVerb(),
}
@ -182,10 +182,10 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
return w.decisionOnError, "", err
}
if entry, ok := w.responseCache.Get(string(key)); ok {
r.Status = entry.(authorization.SubjectAccessReviewStatus)
r.Status = entry.(authorizationv1.SubjectAccessReviewStatus)
} else {
var (
result *authorization.SubjectAccessReview
result *authorizationv1.SubjectAccessReview
err error
)
webhook.WithExponentialBackoff(ctx, w.initialBackoff, func() error {
@ -229,13 +229,13 @@ func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]author
return resourceRules, nonResourceRules, incomplete, fmt.Errorf("webhook authorizer does not support user rule resolution")
}
func convertToSARExtra(extra map[string][]string) map[string]authorization.ExtraValue {
func convertToSARExtra(extra map[string][]string) map[string]authorizationv1.ExtraValue {
if extra == nil {
return nil
}
ret := map[string]authorization.ExtraValue{}
ret := map[string]authorizationv1.ExtraValue{}
for k, v := range extra {
ret[k] = authorization.ExtraValue(v)
ret[k] = authorizationv1.ExtraValue(v)
}
return ret
@ -244,36 +244,69 @@ func convertToSARExtra(extra map[string][]string) map[string]authorization.Extra
// subjectAccessReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file,
// and returns a SubjectAccessReviewInterface that uses that client. Note that the client submits SubjectAccessReview
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string) (authorizationclient.SubjectAccessReviewInterface, error) {
func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string, version string) (subjectAccessReviewer, error) {
localScheme := runtime.NewScheme()
if err := scheme.AddToScheme(localScheme); err != nil {
return nil, err
}
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
switch version {
case authorizationv1.SchemeGroupVersion.Version:
groupVersions := []schema.GroupVersion{authorizationv1.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 &subjectAccessReviewV1Client{gw}, nil
case authorizationv1beta1.SchemeGroupVersion.Version:
groupVersions := []schema.GroupVersion{authorizationv1beta1.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 &subjectAccessReviewV1beta1Client{gw}, nil
default:
return nil, fmt.Errorf(
"unsupported webhook authorizer version %q, supported versions are %q, %q",
version,
authorizationv1.SchemeGroupVersion.Version,
authorizationv1beta1.SchemeGroupVersion.Version,
)
}
return &subjectAccessReviewClient{gw}, nil
}
type subjectAccessReviewClient struct {
type subjectAccessReviewV1Client struct {
w *webhook.GenericWebhook
}
func (t *subjectAccessReviewClient) Create(subjectAccessReview *authorization.SubjectAccessReview) (*authorization.SubjectAccessReview, error) {
return t.CreateContext(context.Background(), subjectAccessReview)
}
func (t *subjectAccessReviewClient) CreateContext(ctx context.Context, subjectAccessReview *authorization.SubjectAccessReview) (*authorization.SubjectAccessReview, error) {
result := &authorization.SubjectAccessReview{}
func (t *subjectAccessReviewV1Client) CreateContext(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview) (*authorizationv1.SubjectAccessReview, error) {
result := &authorizationv1.SubjectAccessReview{}
err := t.w.RestClient.Post().Context(ctx).Body(subjectAccessReview).Do().Into(result)
return result, err
}
type subjectAccessReviewV1beta1Client struct {
w *webhook.GenericWebhook
}
func (t *subjectAccessReviewV1beta1Client) CreateContext(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview) (*authorizationv1.SubjectAccessReview, error) {
v1beta1Review := &authorizationv1beta1.SubjectAccessReview{Spec: v1SpecToV1beta1Spec(&subjectAccessReview.Spec)}
v1beta1Result := &authorizationv1beta1.SubjectAccessReview{}
err := t.w.RestClient.Post().Context(ctx).Body(v1beta1Review).Do().Into(v1beta1Result)
if err == nil {
subjectAccessReview.Status = v1beta1StatusToV1Status(&v1beta1Result.Status)
}
return subjectAccessReview, err
}
// shouldCache determines whether it is safe to cache the given request attributes. If the
// requester-controlled attributes are too large, this may be a DoS attempt, so we skip the cache.
func shouldCache(attr authorizer.Attributes) bool {
@ -287,3 +320,59 @@ func shouldCache(attr authorizer.Attributes) bool {
int64(len(attr.GetPath()))
return controlledAttrSize < maxControlledAttrCacheSize
}
func v1beta1StatusToV1Status(in *authorizationv1beta1.SubjectAccessReviewStatus) authorizationv1.SubjectAccessReviewStatus {
return authorizationv1.SubjectAccessReviewStatus{
Allowed: in.Allowed,
Denied: in.Denied,
Reason: in.Reason,
EvaluationError: in.EvaluationError,
}
}
func v1SpecToV1beta1Spec(in *authorizationv1.SubjectAccessReviewSpec) authorizationv1beta1.SubjectAccessReviewSpec {
return authorizationv1beta1.SubjectAccessReviewSpec{
ResourceAttributes: v1ResourceAttributesToV1beta1ResourceAttributes(in.ResourceAttributes),
NonResourceAttributes: v1NonResourceAttributesToV1beta1NonResourceAttributes(in.NonResourceAttributes),
User: in.User,
Groups: in.Groups,
Extra: v1ExtraToV1beta1Extra(in.Extra),
UID: in.UID,
}
}
func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.ResourceAttributes) *authorizationv1beta1.ResourceAttributes {
if in == nil {
return nil
}
return &authorizationv1beta1.ResourceAttributes{
Namespace: in.Namespace,
Verb: in.Verb,
Group: in.Group,
Version: in.Version,
Resource: in.Resource,
Subresource: in.Subresource,
Name: in.Name,
}
}
func v1NonResourceAttributesToV1beta1NonResourceAttributes(in *authorizationv1.NonResourceAttributes) *authorizationv1beta1.NonResourceAttributes {
if in == nil {
return nil
}
return &authorizationv1beta1.NonResourceAttributes{
Path: in.Path,
Verb: in.Verb,
}
}
func v1ExtraToV1beta1Extra(in map[string]authorizationv1.ExtraValue) map[string]authorizationv1beta1.ExtraValue {
if in == nil {
return nil
}
ret := make(map[string]authorizationv1beta1.ExtraValue, len(in))
for k, v := range in {
ret[k] = authorizationv1beta1.ExtraValue(v)
}
return ret
}

View File

@ -34,15 +34,15 @@ import (
"text/template"
"time"
"k8s.io/api/authorization/v1beta1"
authorizationv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/tools/clientcmd/api/v1"
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
)
func TestNewFromConfig(t *testing.T) {
func TestV1NewFromConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
@ -186,7 +186,7 @@ current-context: default
return fmt.Errorf("failed to execute test template: %v", err)
}
// Create a new authorizer
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p)
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1")
if err != nil {
return fmt.Errorf("error building sar client: %v", err)
}
@ -202,14 +202,14 @@ current-context: default
}
}
// Service mocks a remote service.
type Service interface {
Review(*v1beta1.SubjectAccessReview)
// V1Service mocks a remote service.
type V1Service interface {
Review(*authorizationv1.SubjectAccessReview)
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 {
@ -240,7 +240,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
return
}
var review v1beta1.SubjectAccessReview
var review authorizationv1.SubjectAccessReview
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)
@ -248,7 +248,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
}
// ensure we received the serialized review as expected
if review.APIVersion != "authorization.k8s.io/v1beta1" {
if review.APIVersion != "authorization.k8s.io/v1" {
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
return
}
@ -267,7 +267,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
APIVersion string `json:"apiVersion"`
Status status `json:"status"`
}{
APIVersion: v1beta1.SchemeGroupVersion.String(),
APIVersion: authorizationv1.SchemeGroupVersion.String(),
Status: status{review.Status.Allowed, review.Status.Reason, review.Status.EvaluationError},
}
w.Header().Set("Content-Type", "application/json")
@ -287,23 +287,23 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
}
// A service that can be set to allow all or deny all authorization requests.
type mockService struct {
type mockV1Service struct {
allow bool
statusCode int
called int
}
func (m *mockService) Review(r *v1beta1.SubjectAccessReview) {
func (m *mockV1Service) Review(r *authorizationv1.SubjectAccessReview) {
m.called++
r.Status.Allowed = m.allow
}
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 }
// newAuthorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
// newV1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
// a new WebhookAuthorizer from it.
func newAuthorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration) (*WebhookAuthorizer, error) {
func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration) (*WebhookAuthorizer, error) {
tempfile, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
@ -325,14 +325,14 @@ func newAuthorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTi
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
return nil, err
}
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p)
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1")
if err != nil {
return nil, fmt.Errorf("error building sar client: %v", err)
}
return newWithBackoff(sarClient, cacheTime, cacheTime, 0)
}
func TestTLSConfig(t *testing.T) {
func TestV1TLSConfig(t *testing.T) {
tests := []struct {
test string
clientCert, clientKey, clientCA []byte
@ -378,17 +378,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 := newAuthorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0)
wh, err := newV1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0)
if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err)
return
@ -427,62 +427,62 @@ func TestTLSConfig(t *testing.T) {
}
}
// recorderService records all access review requests.
type recorderService struct {
last v1beta1.SubjectAccessReview
// recorderV1Service records all access review requests.
type recorderV1Service struct {
last authorizationv1.SubjectAccessReview
err error
}
func (rec *recorderService) Review(r *v1beta1.SubjectAccessReview) {
rec.last = v1beta1.SubjectAccessReview{}
func (rec *recorderV1Service) Review(r *authorizationv1.SubjectAccessReview) {
rec.last = authorizationv1.SubjectAccessReview{}
rec.last = *r
r.Status.Allowed = true
}
func (rec *recorderService) Last() (v1beta1.SubjectAccessReview, error) {
func (rec *recorderV1Service) Last() (authorizationv1.SubjectAccessReview, error) {
return rec.last, rec.err
}
func (rec *recorderService) HTTPStatusCode() int { return 200 }
func (rec *recorderV1Service) HTTPStatusCode() int { return 200 }
func TestWebhook(t *testing.T) {
serv := new(recorderService)
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
func TestV1Webhook(t *testing.T) {
serv := new(recorderV1Service)
s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
if err != nil {
t.Fatal(err)
}
defer s.Close()
wh, err := newAuthorizer(s.URL, clientCert, clientKey, caCert, 0)
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0)
if err != nil {
t.Fatal(err)
}
expTypeMeta := metav1.TypeMeta{
APIVersion: "authorization.k8s.io/v1beta1",
APIVersion: "authorization.k8s.io/v1",
Kind: "SubjectAccessReview",
}
tests := []struct {
attr authorizer.Attributes
want v1beta1.SubjectAccessReview
want authorizationv1.SubjectAccessReview
}{
{
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}},
want: v1beta1.SubjectAccessReview{
want: authorizationv1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: v1beta1.SubjectAccessReviewSpec{
NonResourceAttributes: &v1beta1.NonResourceAttributes{},
Spec: authorizationv1.SubjectAccessReviewSpec{
NonResourceAttributes: &authorizationv1.NonResourceAttributes{},
},
},
},
{
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}},
want: v1beta1.SubjectAccessReview{
want: authorizationv1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: v1beta1.SubjectAccessReviewSpec{
Spec: authorizationv1.SubjectAccessReviewSpec{
User: "jane",
NonResourceAttributes: &v1beta1.NonResourceAttributes{},
NonResourceAttributes: &authorizationv1.NonResourceAttributes{},
},
},
},
@ -503,13 +503,13 @@ func TestWebhook(t *testing.T) {
ResourceRequest: true,
Path: "/foo",
},
want: v1beta1.SubjectAccessReview{
want: authorizationv1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: v1beta1.SubjectAccessReviewSpec{
Spec: authorizationv1.SubjectAccessReviewSpec{
User: "jane",
UID: "1",
Groups: []string{"group1", "group2"},
ResourceAttributes: &v1beta1.ResourceAttributes{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Verb: "GET",
Namespace: "kittensandponies",
Group: "group3",
@ -546,16 +546,16 @@ func TestWebhook(t *testing.T) {
// TestWebhookCache verifies that error responses from the server are not
// cached, but successful responses are.
func TestWebhookCache(t *testing.T) {
serv := new(mockService)
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
func TestV1WebhookCache(t *testing.T) {
serv := new(mockV1Service)
s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
if err != nil {
t.Fatal(err)
}
defer s.Close()
// Create an authorizer that caches successful responses "forever" (100 days).
wh, err := newAuthorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour)
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour)
if err != nil {
t.Fatal(err)
}

View File

@ -0,0 +1,647 @@
/*
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"
"path/filepath"
"reflect"
"strings"
"testing"
"text/template"
"time"
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
)
func TestV1beta1NewFromConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
data := struct {
CA string
Cert string
Key string
}{
CA: filepath.Join(dir, "ca.pem"),
Cert: filepath.Join(dir, "clientcert.pem"),
Key: filepath.Join(dir, "clientkey.pem"),
}
files := []struct {
name string
data []byte
}{
{data.CA, caCert},
{data.Cert, clientCert},
{data.Key, clientKey},
}
for _, file := range files {
if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil {
t.Fatal(err)
}
}
tests := []struct {
msg string
configTmpl string
wantErr bool
}{
{
msg: "a single cluster and single user",
configTmpl: `
clusters:
- cluster:
certificate-authority: {{ .CA }}
server: https://authz.example.com
name: foobar
users:
- name: a cluster
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
`,
wantErr: true,
},
{
msg: "multiple clusters with no context",
configTmpl: `
clusters:
- cluster:
certificate-authority: {{ .CA }}
server: https://authz.example.com
name: foobar
- cluster:
certificate-authority: a bad certificate path
server: https://authz.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
`,
wantErr: true,
},
{
msg: "multiple clusters with a context",
configTmpl: `
clusters:
- cluster:
certificate-authority: a bad certificate path
server: https://authz.example.com
name: foobar
- cluster:
certificate-authority: {{ .CA }}
server: https://authz.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
contexts:
- name: default
context:
cluster: barfoo
user: a name
current-context: default
`,
wantErr: false,
},
{
msg: "cluster with bad certificate path specified",
configTmpl: `
clusters:
- cluster:
certificate-authority: a bad certificate path
server: https://authz.example.com
name: foobar
- cluster:
certificate-authority: {{ .CA }}
server: https://authz.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
contexts:
- name: default
context:
cluster: foobar
user: a name
current-context: default
`,
wantErr: true,
},
}
for _, tt := range tests {
// Use a closure so defer statements trigger between loop iterations.
err := func() error {
tempfile, err := ioutil.TempFile("", "")
if err != nil {
return err
}
p := tempfile.Name()
defer os.Remove(p)
tmpl, err := template.New("test").Parse(tt.configTmpl)
if err != nil {
return fmt.Errorf("failed to parse test template: %v", err)
}
if err := tmpl.Execute(tempfile, data); err != nil {
return fmt.Errorf("failed to execute test template: %v", err)
}
// Create a new authorizer
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1beta1")
if err != nil {
return fmt.Errorf("error building sar client: %v", err)
}
_, err = newWithBackoff(sarClient, 0, 0, 0)
return err
}()
if err != nil && !tt.wantErr {
t.Errorf("failed to load plugin from config %q: %v", tt.msg, err)
}
if err == nil && tt.wantErr {
t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg)
}
}
}
// V1beta1Service mocks a remote service.
type V1beta1Service interface {
Review(*authorizationv1beta1.SubjectAccessReview)
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 authorizationv1beta1.SubjectAccessReview
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 review as expected
if review.APIVersion != "authorization.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 status struct {
Allowed bool `json:"allowed"`
Reason string `json:"reason"`
EvaluationError string `json:"evaluationError"`
}
resp := struct {
APIVersion string `json:"apiVersion"`
Status status `json:"status"`
}{
APIVersion: authorizationv1beta1.SchemeGroupVersion.String(),
Status: status{review.Status.Allowed, review.Status.Reason, review.Status.EvaluationError},
}
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 allow all or deny all authorization requests.
type mockV1beta1Service struct {
allow bool
statusCode int
called int
}
func (m *mockV1beta1Service) Review(r *authorizationv1beta1.SubjectAccessReview) {
m.called++
r.Status.Allowed = m.allow
}
func (m *mockV1beta1Service) Allow() { m.allow = true }
func (m *mockV1beta1Service) Deny() { m.allow = false }
func (m *mockV1beta1Service) HTTPStatusCode() int { return m.statusCode }
// newV1beta1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
// a new WebhookAuthorizer from it.
func newV1beta1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration) (*WebhookAuthorizer, 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: callbackURL, CertificateAuthorityData: ca},
},
},
AuthInfos: []v1.NamedAuthInfo{
{
AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
},
},
}
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
return nil, err
}
sarClient, err := subjectAccessReviewInterfaceFromKubeconfig(p, "v1beta1")
if err != nil {
return nil, fmt.Errorf("error building sar client: %v", err)
}
return newWithBackoff(sarClient, cacheTime, cacheTime, 0)
}
func TestV1beta1TLSConfig(t *testing.T) {
tests := []struct {
test string
clientCert, clientKey, clientCA []byte
serverCert, serverKey, serverCA []byte
wantAuth, wantErr bool
}{
{
test: "TLS setup between client and server",
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
wantAuth: true,
},
{
test: "Server does not require client auth",
clientCA: caCert,
serverCert: serverCert, serverKey: serverKey,
wantAuth: true,
},
{
test: "Server does not require client auth, client provides it",
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
serverCert: serverCert, serverKey: serverKey,
wantAuth: true,
},
{
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 := newV1beta1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0)
if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err)
return
}
attr := authorizer.AttributesRecord{User: &user.DefaultInfo{}}
// Allow all and see if we get an error.
service.Allow()
decision, _, err := wh.Authorize(context.Background(), attr)
if tt.wantAuth {
if decision != authorizer.DecisionAllow {
t.Errorf("expected successful authorization")
}
} else {
if decision == authorizer.DecisionAllow {
t.Errorf("expected failed authorization")
}
}
if tt.wantErr {
if err == nil {
t.Errorf("expected error making authorization request: %v", err)
}
return
}
if err != nil {
t.Errorf("%s: failed to authorize with AllowAll policy: %v", tt.test, err)
return
}
service.Deny()
if decision, _, _ := wh.Authorize(context.Background(), attr); decision == authorizer.DecisionAllow {
t.Errorf("%s: incorrectly authorized with DenyAll policy", tt.test)
}
}()
}
}
// recorderV1beta1Service records all access review requests.
type recorderV1beta1Service struct {
last authorizationv1beta1.SubjectAccessReview
err error
}
func (rec *recorderV1beta1Service) Review(r *authorizationv1beta1.SubjectAccessReview) {
rec.last = authorizationv1beta1.SubjectAccessReview{}
rec.last = *r
r.Status.Allowed = true
}
func (rec *recorderV1beta1Service) Last() (authorizationv1beta1.SubjectAccessReview, error) {
return rec.last, rec.err
}
func (rec *recorderV1beta1Service) HTTPStatusCode() int { return 200 }
func TestV1beta1Webhook(t *testing.T) {
serv := new(recorderV1beta1Service)
s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert)
if err != nil {
t.Fatal(err)
}
defer s.Close()
wh, err := newV1beta1Authorizer(s.URL, clientCert, clientKey, caCert, 0)
if err != nil {
t.Fatal(err)
}
expTypeMeta := metav1.TypeMeta{
APIVersion: "authorization.k8s.io/v1beta1",
Kind: "SubjectAccessReview",
}
tests := []struct {
attr authorizer.Attributes
want authorizationv1beta1.SubjectAccessReview
}{
{
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}},
want: authorizationv1beta1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: authorizationv1beta1.SubjectAccessReviewSpec{
NonResourceAttributes: &authorizationv1beta1.NonResourceAttributes{},
},
},
},
{
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}},
want: authorizationv1beta1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: authorizationv1beta1.SubjectAccessReviewSpec{
User: "jane",
NonResourceAttributes: &authorizationv1beta1.NonResourceAttributes{},
},
},
},
{
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "jane",
UID: "1",
Groups: []string{"group1", "group2"},
},
Verb: "GET",
Namespace: "kittensandponies",
APIGroup: "group3",
APIVersion: "v7beta3",
Resource: "pods",
Subresource: "proxy",
Name: "my-pod",
ResourceRequest: true,
Path: "/foo",
},
want: authorizationv1beta1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: authorizationv1beta1.SubjectAccessReviewSpec{
User: "jane",
UID: "1",
Groups: []string{"group1", "group2"},
ResourceAttributes: &authorizationv1beta1.ResourceAttributes{
Verb: "GET",
Namespace: "kittensandponies",
Group: "group3",
Version: "v7beta3",
Resource: "pods",
Subresource: "proxy",
Name: "my-pod",
},
},
},
},
}
for i, tt := range tests {
decision, _, err := wh.Authorize(context.Background(), tt.attr)
if err != nil {
t.Fatal(err)
}
if decision != authorizer.DecisionAllow {
t.Errorf("case %d: authorization failed", i)
continue
}
gotAttr, err := serv.Last()
if err != nil {
t.Errorf("case %d: failed to deserialize webhook request: %v", i, err)
continue
}
if !reflect.DeepEqual(gotAttr, tt.want) {
t.Errorf("case %d: got != want:\n%s", i, diff.ObjectGoPrintDiff(gotAttr, tt.want))
}
}
}
// TestWebhookCache verifies that error responses from the server are not
// cached, but successful responses are.
func TestV1beta1WebhookCache(t *testing.T) {
serv := new(mockV1beta1Service)
s, err := NewV1beta1TestServer(serv, serverCert, serverKey, caCert)
if err != nil {
t.Fatal(err)
}
defer s.Close()
// Create an authorizer that caches successful responses "forever" (100 days).
wh, err := newV1beta1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour)
if err != nil {
t.Fatal(err)
}
aliceAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "alice"}}
bobAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "bob"}}
aliceRidiculousAttr := authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "alice"},
ResourceRequest: true,
Verb: strings.Repeat("v", 2000),
APIGroup: strings.Repeat("g", 2000),
APIVersion: strings.Repeat("a", 2000),
Resource: strings.Repeat("r", 2000),
Name: strings.Repeat("n", 2000),
}
bobRidiculousAttr := authorizer.AttributesRecord{
User: &user.DefaultInfo{Name: "bob"},
ResourceRequest: true,
Verb: strings.Repeat("v", 2000),
APIGroup: strings.Repeat("g", 2000),
APIVersion: strings.Repeat("a", 2000),
Resource: strings.Repeat("r", 2000),
Name: strings.Repeat("n", 2000),
}
type webhookCacheTestCase struct {
name string
attr authorizer.AttributesRecord
allow bool
statusCode int
expectedErr bool
expectedAuthorized bool
expectedCalls int
}
tests := []webhookCacheTestCase{
// server error and 429's retry
{name: "server errors retry", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
{name: "429s retry", attr: aliceAttr, allow: false, statusCode: 429, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
// regular errors return errors but do not retry
{name: "404 doesnt retry", attr: aliceAttr, allow: false, statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
{name: "403 doesnt retry", attr: aliceAttr, allow: false, statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
{name: "401 doesnt retry", attr: aliceAttr, allow: false, statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
// successful responses are cached
{name: "alice successful request", attr: aliceAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
// later requests within the cache window don't hit the backend
{name: "alice cached request", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCalls: 0},
// a request with different attributes doesn't hit the cache
{name: "bob failed request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
// successful response for other attributes is cached
{name: "bob unauthorized request", attr: bobAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
// later requests within the cache window don't hit the backend
{name: "bob unauthorized cached request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: false, expectedCalls: 0},
// ridiculous unauthorized requests are not cached.
{name: "ridiculous unauthorized request", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
// later ridiculous requests within the cache window still hit the backend
{name: "ridiculous unauthorized request again", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
// ridiculous authorized requests are not cached.
{name: "ridiculous authorized request", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
// later ridiculous requests within the cache window still hit the backend
{name: "ridiculous authorized request again", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
serv.called = 0
serv.allow = test.allow
serv.statusCode = test.statusCode
authorized, _, err := wh.Authorize(context.Background(), test.attr)
if test.expectedErr && err == nil {
t.Fatalf("%d: Expected error", i)
} else if !test.expectedErr && err != nil {
t.Fatalf("%d: unexpected error: %v", i, err)
}
if test.expectedAuthorized != (authorized == authorizer.DecisionAllow) {
t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedAuthorized, authorized)
}
if test.expectedCalls != serv.called {
t.Errorf("%d: expected %d calls, got %d", i, test.expectedCalls, serv.called)
}
})
}
}

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
}