Switch kubelet/aggregated API servers to use v1 subjectaccessreviews

This commit is contained in:
Jordan Liggitt 2019-11-04 23:29:56 -05:00
parent 5ef4fe959a
commit d54a70db5c
10 changed files with 948 additions and 90 deletions

View File

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

View File

@ -29,7 +29,7 @@ import (
"k8s.io/apiserver/pkg/server/dynamiccertificates"
clientset "k8s.io/client-go/kubernetes"
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
"k8s.io/kubernetes/pkg/kubelet/server"
@ -44,7 +44,7 @@ func BuildAuth(nodeName types.NodeName, client clientset.Interface, config kubel
)
if client != nil && !reflect.ValueOf(client).IsNil() {
tokenClient = client.AuthenticationV1().TokenReviews()
sarClient = client.AuthorizationV1beta1().SubjectAccessReviews()
sarClient = client.AuthorizationV1().SubjectAccessReviews()
}
authenticator, err := BuildAuthn(tokenClient, config.Authentication)

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

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

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

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

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