k8s.io/apiserver@v0.31.1/plugin/pkg/authorizer/webhook/webhook_v1_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package webhook
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"net/url"
    29  	"os"
    30  	"path/filepath"
    31  	"reflect"
    32  	"strings"
    33  	"testing"
    34  	"text/template"
    35  	"time"
    36  
    37  	utiltesting "k8s.io/client-go/util/testing"
    38  
    39  	"github.com/google/go-cmp/cmp"
    40  
    41  	authorizationv1 "k8s.io/api/authorization/v1"
    42  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    43  	"k8s.io/apimachinery/pkg/fields"
    44  	"k8s.io/apimachinery/pkg/labels"
    45  	"k8s.io/apimachinery/pkg/selection"
    46  	"k8s.io/apimachinery/pkg/util/wait"
    47  	"k8s.io/apiserver/pkg/apis/apiserver"
    48  	"k8s.io/apiserver/pkg/authentication/user"
    49  	"k8s.io/apiserver/pkg/authorization/authorizer"
    50  	celmetrics "k8s.io/apiserver/pkg/authorization/cel"
    51  	"k8s.io/apiserver/pkg/features"
    52  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    53  	webhookutil "k8s.io/apiserver/pkg/util/webhook"
    54  	"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
    55  	v1 "k8s.io/client-go/tools/clientcmd/api/v1"
    56  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    57  	"k8s.io/component-base/metrics/legacyregistry"
    58  	"k8s.io/component-base/metrics/testutil"
    59  )
    60  
    61  var testRetryBackoff = wait.Backoff{
    62  	Duration: 5 * time.Millisecond,
    63  	Factor:   1.5,
    64  	Jitter:   0.2,
    65  	Steps:    5,
    66  }
    67  
    68  func TestV1NewFromConfig(t *testing.T) {
    69  	dir, err := ioutil.TempDir("", "")
    70  	if err != nil {
    71  		t.Fatal(err)
    72  	}
    73  	defer os.RemoveAll(dir)
    74  
    75  	data := struct {
    76  		CA   string
    77  		Cert string
    78  		Key  string
    79  	}{
    80  		CA:   filepath.Join(dir, "ca.pem"),
    81  		Cert: filepath.Join(dir, "clientcert.pem"),
    82  		Key:  filepath.Join(dir, "clientkey.pem"),
    83  	}
    84  
    85  	files := []struct {
    86  		name string
    87  		data []byte
    88  	}{
    89  		{data.CA, caCert},
    90  		{data.Cert, clientCert},
    91  		{data.Key, clientKey},
    92  	}
    93  	for _, file := range files {
    94  		if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil {
    95  			t.Fatal(err)
    96  		}
    97  	}
    98  
    99  	tests := []struct {
   100  		msg        string
   101  		configTmpl string
   102  		wantErr    bool
   103  	}{
   104  		{
   105  			msg: "a single cluster and single user",
   106  			configTmpl: `
   107  clusters:
   108  - cluster:
   109      certificate-authority: {{ .CA }}
   110      server: https://authz.example.com
   111    name: foobar
   112  users:
   113  - name: a cluster
   114    user:
   115      client-certificate: {{ .Cert }}
   116      client-key: {{ .Key }}
   117  `,
   118  			wantErr: true,
   119  		},
   120  		{
   121  			msg: "multiple clusters with no context",
   122  			configTmpl: `
   123  clusters:
   124  - cluster:
   125      certificate-authority: {{ .CA }}
   126      server: https://authz.example.com
   127    name: foobar
   128  - cluster:
   129      certificate-authority: a bad certificate path
   130      server: https://authz.example.com
   131    name: barfoo
   132  users:
   133  - name: a name
   134    user:
   135      client-certificate: {{ .Cert }}
   136      client-key: {{ .Key }}
   137  `,
   138  			wantErr: true,
   139  		},
   140  		{
   141  			msg: "multiple clusters with a context",
   142  			configTmpl: `
   143  clusters:
   144  - cluster:
   145      certificate-authority: a bad certificate path
   146      server: https://authz.example.com
   147    name: foobar
   148  - cluster:
   149      certificate-authority: {{ .CA }}
   150      server: https://authz.example.com
   151    name: barfoo
   152  users:
   153  - name: a name
   154    user:
   155      client-certificate: {{ .Cert }}
   156      client-key: {{ .Key }}
   157  contexts:
   158  - name: default
   159    context:
   160      cluster: barfoo
   161      user: a name
   162  current-context: default
   163  `,
   164  			wantErr: false,
   165  		},
   166  		{
   167  			msg: "cluster with bad certificate path specified",
   168  			configTmpl: `
   169  clusters:
   170  - cluster:
   171      certificate-authority: a bad certificate path
   172      server: https://authz.example.com
   173    name: foobar
   174  - cluster:
   175      certificate-authority: {{ .CA }}
   176      server: https://authz.example.com
   177    name: barfoo
   178  users:
   179  - name: a name
   180    user:
   181      client-certificate: {{ .Cert }}
   182      client-key: {{ .Key }}
   183  contexts:
   184  - name: default
   185    context:
   186      cluster: foobar
   187      user: a name
   188  current-context: default
   189  `,
   190  			wantErr: true,
   191  		},
   192  	}
   193  
   194  	for _, tt := range tests {
   195  		// Use a closure so defer statements trigger between loop iterations.
   196  		err := func() error {
   197  			tempfile, err := ioutil.TempFile("", "")
   198  			if err != nil {
   199  				return err
   200  			}
   201  			p := tempfile.Name()
   202  			defer utiltesting.CloseAndRemove(t, tempfile)
   203  
   204  			tmpl, err := template.New("test").Parse(tt.configTmpl)
   205  			if err != nil {
   206  				return fmt.Errorf("failed to parse test template: %v", err)
   207  			}
   208  			if err := tmpl.Execute(tempfile, data); err != nil {
   209  				return fmt.Errorf("failed to execute test template: %v", err)
   210  			}
   211  			// Create a new authorizer
   212  			clientConfig, err := webhookutil.LoadKubeconfig(p, nil)
   213  			if err != nil {
   214  				return err
   215  			}
   216  			sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1", testRetryBackoff)
   217  			if err != nil {
   218  				return fmt.Errorf("error building sar client: %v", err)
   219  			}
   220  			_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []apiserver.WebhookMatchCondition{}, noopAuthorizerMetrics(), "")
   221  			return err
   222  		}()
   223  		if err != nil && !tt.wantErr {
   224  			t.Errorf("failed to load plugin from config %q: %v", tt.msg, err)
   225  		}
   226  		if err == nil && tt.wantErr {
   227  			t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg)
   228  		}
   229  	}
   230  }
   231  
   232  // V1Service mocks a remote service.
   233  type V1Service interface {
   234  	Review(*authorizationv1.SubjectAccessReview)
   235  	HTTPStatusCode() int
   236  }
   237  
   238  // NewV1TestServer wraps a V1Service as an httptest.Server.
   239  func NewV1TestServer(s V1Service, cert, key, caCert []byte) (*httptest.Server, error) {
   240  	const webhookPath = "/testserver"
   241  	var tlsConfig *tls.Config
   242  	if cert != nil {
   243  		cert, err := tls.X509KeyPair(cert, key)
   244  		if err != nil {
   245  			return nil, err
   246  		}
   247  		tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
   248  	}
   249  
   250  	if caCert != nil {
   251  		rootCAs := x509.NewCertPool()
   252  		rootCAs.AppendCertsFromPEM(caCert)
   253  		if tlsConfig == nil {
   254  			tlsConfig = &tls.Config{}
   255  		}
   256  		tlsConfig.ClientCAs = rootCAs
   257  		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
   258  	}
   259  
   260  	serveHTTP := func(w http.ResponseWriter, r *http.Request) {
   261  		if r.Method != "POST" {
   262  			http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed)
   263  			return
   264  		}
   265  		if r.URL.Path != webhookPath {
   266  			http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound)
   267  			return
   268  		}
   269  
   270  		var review authorizationv1.SubjectAccessReview
   271  		bodyData, _ := ioutil.ReadAll(r.Body)
   272  		if err := json.Unmarshal(bodyData, &review); err != nil {
   273  			http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
   274  			return
   275  		}
   276  
   277  		// ensure we received the serialized review as expected
   278  		if review.APIVersion != "authorization.k8s.io/v1" {
   279  			http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
   280  			return
   281  		}
   282  		// once we have a successful request, always call the review to record that we were called
   283  		s.Review(&review)
   284  		if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 {
   285  			http.Error(w, "HTTP Error", s.HTTPStatusCode())
   286  			return
   287  		}
   288  		type status struct {
   289  			Allowed         bool   `json:"allowed"`
   290  			Reason          string `json:"reason"`
   291  			EvaluationError string `json:"evaluationError"`
   292  		}
   293  		resp := struct {
   294  			APIVersion string `json:"apiVersion"`
   295  			Status     status `json:"status"`
   296  		}{
   297  			APIVersion: authorizationv1.SchemeGroupVersion.String(),
   298  			Status:     status{review.Status.Allowed, review.Status.Reason, review.Status.EvaluationError},
   299  		}
   300  		w.Header().Set("Content-Type", "application/json")
   301  		json.NewEncoder(w).Encode(resp)
   302  	}
   303  
   304  	server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
   305  	server.TLS = tlsConfig
   306  	server.StartTLS()
   307  
   308  	// Adjust the path to point to our custom path
   309  	serverURL, _ := url.Parse(server.URL)
   310  	serverURL.Path = webhookPath
   311  	server.URL = serverURL.String()
   312  
   313  	return server, nil
   314  }
   315  
   316  // A service that can be set to allow all or deny all authorization requests.
   317  type mockV1Service struct {
   318  	allow      bool
   319  	statusCode int
   320  	called     int
   321  
   322  	// reviewHook is called just before returning from the Review() method
   323  	reviewHook func(*authorizationv1.SubjectAccessReview)
   324  }
   325  
   326  func (m *mockV1Service) Review(r *authorizationv1.SubjectAccessReview) {
   327  	m.called++
   328  	r.Status.Allowed = m.allow
   329  
   330  	if m.reviewHook != nil {
   331  		m.reviewHook(r)
   332  	}
   333  }
   334  func (m *mockV1Service) Allow()              { m.allow = true }
   335  func (m *mockV1Service) Deny()               { m.allow = false }
   336  func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode }
   337  
   338  // newV1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
   339  // a new WebhookAuthorizer from it.
   340  func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, metrics metrics.AuthorizerMetrics, expressions []apiserver.WebhookMatchCondition, authzName string) (*WebhookAuthorizer, error) {
   341  	tempfile, err := ioutil.TempFile("", "")
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	p := tempfile.Name()
   346  	defer os.Remove(p)
   347  	config := v1.Config{
   348  		Clusters: []v1.NamedCluster{
   349  			{
   350  				Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca},
   351  			},
   352  		},
   353  		AuthInfos: []v1.NamedAuthInfo{
   354  			{
   355  				AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
   356  			},
   357  		},
   358  	}
   359  	if err := json.NewEncoder(tempfile).Encode(config); err != nil {
   360  		return nil, err
   361  	}
   362  	clientConfig, err := webhookutil.LoadKubeconfig(p, nil)
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  	sarClient, err := subjectAccessReviewInterfaceFromConfig(clientConfig, "v1", testRetryBackoff)
   367  	if err != nil {
   368  		return nil, fmt.Errorf("error building sar client: %v", err)
   369  	}
   370  	return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, expressions, metrics, authzName)
   371  }
   372  
   373  func TestV1TLSConfig(t *testing.T) {
   374  	tests := []struct {
   375  		test                            string
   376  		clientCert, clientKey, clientCA []byte
   377  		serverCert, serverKey, serverCA []byte
   378  		wantAuth, wantErr               bool
   379  	}{
   380  		{
   381  			test:       "TLS setup between client and server",
   382  			clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
   383  			serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
   384  			wantAuth: true,
   385  		},
   386  		{
   387  			test:       "Server does not require client auth",
   388  			clientCA:   caCert,
   389  			serverCert: serverCert, serverKey: serverKey,
   390  			wantAuth: true,
   391  		},
   392  		{
   393  			test:       "Server does not require client auth, client provides it",
   394  			clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
   395  			serverCert: serverCert, serverKey: serverKey,
   396  			wantAuth: true,
   397  		},
   398  		{
   399  			test:       "Client does not trust server",
   400  			clientCert: clientCert, clientKey: clientKey,
   401  			serverCert: serverCert, serverKey: serverKey,
   402  			wantErr: true,
   403  		},
   404  		{
   405  			test:       "Server does not trust client",
   406  			clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
   407  			serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
   408  			wantErr: true,
   409  		},
   410  		{
   411  			// Plugin does not support insecure configurations.
   412  			test:    "Server is using insecure connection",
   413  			wantErr: true,
   414  		},
   415  	}
   416  	for _, tt := range tests {
   417  		// Use a closure so defer statements trigger between loop iterations.
   418  		func() {
   419  			service := new(mockV1Service)
   420  			service.statusCode = 200
   421  
   422  			server, err := NewV1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
   423  			if err != nil {
   424  				t.Errorf("%s: failed to create server: %v", tt.test, err)
   425  				return
   426  			}
   427  			defer server.Close()
   428  
   429  			wh, err := newV1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, noopAuthorizerMetrics(), []apiserver.WebhookMatchCondition{}, "")
   430  			if err != nil {
   431  				t.Errorf("%s: failed to create client: %v", tt.test, err)
   432  				return
   433  			}
   434  
   435  			attr := authorizer.AttributesRecord{User: &user.DefaultInfo{}}
   436  
   437  			// Allow all and see if we get an error.
   438  			service.Allow()
   439  			decision, _, err := wh.Authorize(context.Background(), attr)
   440  			if tt.wantAuth {
   441  				if decision != authorizer.DecisionAllow {
   442  					t.Errorf("expected successful authorization")
   443  				}
   444  			} else {
   445  				if decision == authorizer.DecisionAllow {
   446  					t.Errorf("expected failed authorization")
   447  				}
   448  			}
   449  			if tt.wantErr {
   450  				if err == nil {
   451  					t.Errorf("expected error making authorization request: %v", err)
   452  				}
   453  				return
   454  			}
   455  			if err != nil {
   456  				t.Errorf("%s: failed to authorize with AllowAll policy: %v", tt.test, err)
   457  				return
   458  			}
   459  
   460  			service.Deny()
   461  			if decision, _, _ := wh.Authorize(context.Background(), attr); decision == authorizer.DecisionAllow {
   462  				t.Errorf("%s: incorrectly authorized with DenyAll policy", tt.test)
   463  			}
   464  		}()
   465  	}
   466  }
   467  
   468  // recorderV1Service records all access review requests.
   469  type recorderV1Service struct {
   470  	last authorizationv1.SubjectAccessReview
   471  	err  error
   472  }
   473  
   474  func (rec *recorderV1Service) Review(r *authorizationv1.SubjectAccessReview) {
   475  	rec.last = authorizationv1.SubjectAccessReview{}
   476  	rec.last = *r
   477  	r.Status.Allowed = true
   478  }
   479  
   480  func (rec *recorderV1Service) Last() (authorizationv1.SubjectAccessReview, error) {
   481  	return rec.last, rec.err
   482  }
   483  
   484  func (rec *recorderV1Service) HTTPStatusCode() int { return 200 }
   485  
   486  func TestV1Webhook(t *testing.T) {
   487  	serv := new(recorderV1Service)
   488  	s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
   489  	if err != nil {
   490  		t.Fatal(err)
   491  	}
   492  	defer s.Close()
   493  
   494  	wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), []apiserver.WebhookMatchCondition{}, "")
   495  	if err != nil {
   496  		t.Fatal(err)
   497  	}
   498  
   499  	expTypeMeta := metav1.TypeMeta{
   500  		APIVersion: "authorization.k8s.io/v1",
   501  		Kind:       "SubjectAccessReview",
   502  	}
   503  
   504  	tests := []struct {
   505  		attr authorizer.Attributes
   506  		want authorizationv1.SubjectAccessReview
   507  	}{
   508  		{
   509  			attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}},
   510  			want: authorizationv1.SubjectAccessReview{
   511  				TypeMeta: expTypeMeta,
   512  				Spec: authorizationv1.SubjectAccessReviewSpec{
   513  					NonResourceAttributes: &authorizationv1.NonResourceAttributes{},
   514  				},
   515  			},
   516  		},
   517  		{
   518  			attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}},
   519  			want: authorizationv1.SubjectAccessReview{
   520  				TypeMeta: expTypeMeta,
   521  				Spec: authorizationv1.SubjectAccessReviewSpec{
   522  					User:                  "jane",
   523  					NonResourceAttributes: &authorizationv1.NonResourceAttributes{},
   524  				},
   525  			},
   526  		},
   527  		{
   528  			attr: authorizer.AttributesRecord{
   529  				User: &user.DefaultInfo{
   530  					Name:   "jane",
   531  					UID:    "1",
   532  					Groups: []string{"group1", "group2"},
   533  				},
   534  				Verb:            "GET",
   535  				Namespace:       "kittensandponies",
   536  				APIGroup:        "group3",
   537  				APIVersion:      "v7beta3",
   538  				Resource:        "pods",
   539  				Subresource:     "proxy",
   540  				Name:            "my-pod",
   541  				ResourceRequest: true,
   542  				Path:            "/foo",
   543  			},
   544  			want: authorizationv1.SubjectAccessReview{
   545  				TypeMeta: expTypeMeta,
   546  				Spec: authorizationv1.SubjectAccessReviewSpec{
   547  					User:   "jane",
   548  					UID:    "1",
   549  					Groups: []string{"group1", "group2"},
   550  					ResourceAttributes: &authorizationv1.ResourceAttributes{
   551  						Verb:        "GET",
   552  						Namespace:   "kittensandponies",
   553  						Group:       "group3",
   554  						Version:     "v7beta3",
   555  						Resource:    "pods",
   556  						Subresource: "proxy",
   557  						Name:        "my-pod",
   558  					},
   559  				},
   560  			},
   561  		},
   562  	}
   563  
   564  	for i, tt := range tests {
   565  		decision, _, err := wh.Authorize(context.Background(), tt.attr)
   566  		if err != nil {
   567  			t.Fatal(err)
   568  		}
   569  		if decision != authorizer.DecisionAllow {
   570  			t.Errorf("case %d: authorization failed", i)
   571  			continue
   572  		}
   573  
   574  		gotAttr, err := serv.Last()
   575  		if err != nil {
   576  			t.Errorf("case %d: failed to deserialize webhook request: %v", i, err)
   577  			continue
   578  		}
   579  		if !reflect.DeepEqual(gotAttr, tt.want) {
   580  			t.Errorf("case %d: got != want:\n%s", i, cmp.Diff(gotAttr, tt.want))
   581  		}
   582  	}
   583  }
   584  
   585  // TestWebhookCache verifies that error responses from the server are not
   586  // cached, but successful responses are.
   587  func TestV1WebhookCache(t *testing.T) {
   588  	serv := new(mockV1Service)
   589  	s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
   590  	if err != nil {
   591  		t.Fatal(err)
   592  	}
   593  	defer s.Close()
   594  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)
   595  	expressions := []apiserver.WebhookMatchCondition{
   596  		{
   597  			Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
   598  		},
   599  	}
   600  	// Create an authorizer that caches successful responses "forever" (100 days).
   601  	wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, noopAuthorizerMetrics(), expressions, "")
   602  	if err != nil {
   603  		t.Fatal(err)
   604  	}
   605  
   606  	aliceAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "alice"}, ResourceRequest: true, Namespace: "kittensandponies"}
   607  	bobAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "bob"}, ResourceRequest: true, Namespace: "kittensandponies"}
   608  	aliceRidiculousAttr := authorizer.AttributesRecord{
   609  		User:            &user.DefaultInfo{Name: "alice"},
   610  		ResourceRequest: true,
   611  		Verb:            strings.Repeat("v", 2000),
   612  		APIGroup:        strings.Repeat("g", 2000),
   613  		APIVersion:      strings.Repeat("a", 2000),
   614  		Resource:        strings.Repeat("r", 2000),
   615  		Name:            strings.Repeat("n", 2000),
   616  		Namespace:       "kittensandponies",
   617  	}
   618  	bobRidiculousAttr := authorizer.AttributesRecord{
   619  		User:            &user.DefaultInfo{Name: "bob"},
   620  		ResourceRequest: true,
   621  		Verb:            strings.Repeat("v", 2000),
   622  		APIGroup:        strings.Repeat("g", 2000),
   623  		APIVersion:      strings.Repeat("a", 2000),
   624  		Resource:        strings.Repeat("r", 2000),
   625  		Name:            strings.Repeat("n", 2000),
   626  		Namespace:       "kittensandponies",
   627  	}
   628  
   629  	type webhookCacheTestCase struct {
   630  		name string
   631  
   632  		attr authorizer.AttributesRecord
   633  
   634  		allow      bool
   635  		statusCode int
   636  
   637  		expectedErr        bool
   638  		expectedAuthorized bool
   639  		expectedCalls      int
   640  	}
   641  
   642  	tests := []webhookCacheTestCase{
   643  		// server error and 429's retry
   644  		{name: "server errors retry", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
   645  		{name: "429s retry", attr: aliceAttr, allow: false, statusCode: 429, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
   646  		// regular errors return errors but do not retry
   647  		{name: "404 doesnt retry", attr: aliceAttr, allow: false, statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
   648  		{name: "403 doesnt retry", attr: aliceAttr, allow: false, statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
   649  		{name: "401 doesnt retry", attr: aliceAttr, allow: false, statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCalls: 1},
   650  		// successful responses are cached
   651  		{name: "alice successful request", attr: aliceAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
   652  		// later requests within the cache window don't hit the backend
   653  		{name: "alice cached request", attr: aliceAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCalls: 0},
   654  
   655  		// a request with different attributes doesn't hit the cache
   656  		{name: "bob failed request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCalls: 5},
   657  		// successful response for other attributes is cached
   658  		{name: "bob unauthorized request", attr: bobAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
   659  		// later requests within the cache window don't hit the backend
   660  		{name: "bob unauthorized cached request", attr: bobAttr, allow: false, statusCode: 500, expectedErr: false, expectedAuthorized: false, expectedCalls: 0},
   661  		// ridiculous unauthorized requests are not cached.
   662  		{name: "ridiculous unauthorized request", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
   663  		// later ridiculous requests within the cache window still hit the backend
   664  		{name: "ridiculous unauthorized request again", attr: bobRidiculousAttr, allow: false, statusCode: 200, expectedErr: false, expectedAuthorized: false, expectedCalls: 1},
   665  		// ridiculous authorized requests are not cached.
   666  		{name: "ridiculous authorized request", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
   667  		// later ridiculous requests within the cache window still hit the backend
   668  		{name: "ridiculous authorized request again", attr: aliceRidiculousAttr, allow: true, statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCalls: 1},
   669  	}
   670  
   671  	for i, test := range tests {
   672  		t.Run(test.name, func(t *testing.T) {
   673  			serv.called = 0
   674  			serv.allow = test.allow
   675  			serv.statusCode = test.statusCode
   676  			authorized, _, err := wh.Authorize(context.Background(), test.attr)
   677  			if test.expectedErr && err == nil {
   678  				t.Fatalf("%d: Expected error", i)
   679  			} else if !test.expectedErr && err != nil {
   680  				t.Fatalf("%d: unexpected error: %v", i, err)
   681  			}
   682  
   683  			if test.expectedAuthorized != (authorized == authorizer.DecisionAllow) {
   684  				t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedAuthorized, authorized)
   685  			}
   686  
   687  			if test.expectedCalls != serv.called {
   688  				t.Errorf("%d: expected %d calls, got %d", i, test.expectedCalls, serv.called)
   689  			}
   690  		})
   691  	}
   692  }
   693  
   694  // TestStructuredAuthzConfigFeatureEnablement verifies cel expressions can only be used when feature is enabled
   695  func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
   696  
   697  	service := new(mockV1Service)
   698  	service.statusCode = 200
   699  	service.Allow()
   700  	s, err := NewV1TestServer(service, serverCert, serverKey, caCert)
   701  	if err != nil {
   702  		t.Fatal(err)
   703  	}
   704  	defer s.Close()
   705  
   706  	labelRequirement, _ := labels.NewRequirement("baz", selection.Equals, []string{"qux"})
   707  
   708  	type webhookMatchConditionsTestCase struct {
   709  		name               string
   710  		attr               authorizer.AttributesRecord
   711  		allow              bool
   712  		expectedCompileErr bool
   713  		expectedEvalErr    bool
   714  		expectedDecision   authorizer.Decision
   715  		expressions        []apiserver.WebhookMatchCondition
   716  		featureEnabled     bool
   717  		selectorEnabled    bool
   718  	}
   719  	aliceAttr := authorizer.AttributesRecord{
   720  		User: &user.DefaultInfo{
   721  			Name:   "alice",
   722  			UID:    "1",
   723  			Groups: []string{"group1", "group2"},
   724  			Extra:  map[string][]string{"key1": {"a", "b", "c"}},
   725  		},
   726  		ResourceRequest: true,
   727  		Namespace:       "kittensandponies",
   728  		Verb:            "get",
   729  	}
   730  	aliceWithSelectorsAttr := authorizer.AttributesRecord{
   731  		User: &user.DefaultInfo{
   732  			Name:   "alice",
   733  			UID:    "1",
   734  			Groups: []string{"group1", "group2"},
   735  			Extra:  map[string][]string{"key1": {"a", "b", "c"}},
   736  		},
   737  		ResourceRequest:           true,
   738  		Namespace:                 "kittensandponies",
   739  		Verb:                      "get",
   740  		FieldSelectorRequirements: fields.Requirements{fields.Requirement{Field: "foo", Operator: selection.Equals, Value: "bar"}},
   741  		LabelSelectorRequirements: labels.Requirements{*labelRequirement},
   742  	}
   743  	tests := []webhookMatchConditionsTestCase{
   744  		{
   745  			name:               "no match condition does not require feature enablement",
   746  			attr:               aliceAttr,
   747  			allow:              true,
   748  			expectedCompileErr: false,
   749  			expectedDecision:   authorizer.DecisionAllow,
   750  			expressions:        []apiserver.WebhookMatchCondition{},
   751  			featureEnabled:     false,
   752  		},
   753  		{
   754  			name:               "should fail when match conditions are used without feature enabled",
   755  			attr:               aliceAttr,
   756  			allow:              false,
   757  			expectedCompileErr: true,
   758  			expectedDecision:   authorizer.DecisionNoOpinion,
   759  			expressions: []apiserver.WebhookMatchCondition{
   760  				{
   761  					Expression: "request.user == 'alice'",
   762  				},
   763  			},
   764  			featureEnabled: false,
   765  		},
   766  		{
   767  			name:               "feature enabled, match all against all expressions",
   768  			attr:               aliceWithSelectorsAttr,
   769  			allow:              true,
   770  			expectedCompileErr: false,
   771  			expectedDecision:   authorizer.DecisionAllow,
   772  			expressions: []apiserver.WebhookMatchCondition{
   773  				{
   774  					Expression: "request.user == 'alice'",
   775  				},
   776  				{
   777  					Expression: "request.uid == '1'",
   778  				},
   779  				{
   780  					Expression: "('group1' in request.groups)",
   781  				},
   782  				{
   783  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
   784  				},
   785  				{
   786  					Expression: "request.?resourceAttributes.fieldSelector.requirements.orValue([]).exists(r, r.key=='foo' && r.operator=='In' && ('bar' in r.values))",
   787  				},
   788  				{
   789  					Expression: "request.?resourceAttributes.labelSelector.requirements.orValue([]).exists(r, r.key=='baz' && r.operator=='In' && ('qux' in r.values))",
   790  				},
   791  				{
   792  					Expression: "request.resourceAttributes.?labelSelector.requirements.orValue([]).exists(r, r.key=='baz' && r.operator=='In' && ('qux' in r.values))",
   793  				},
   794  				{
   795  					Expression: "request.resourceAttributes.labelSelector.?requirements.orValue([]).exists(r, r.key=='baz' && r.operator=='In' && ('qux' in r.values))",
   796  				},
   797  			},
   798  			featureEnabled:  true,
   799  			selectorEnabled: true,
   800  		},
   801  	}
   802  
   803  	for i, test := range tests {
   804  		t.Run(test.name, func(t *testing.T) {
   805  			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, test.featureEnabled)
   806  			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AuthorizeWithSelectors, test.selectorEnabled)
   807  			wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions, "")
   808  			if test.expectedCompileErr && err == nil {
   809  				t.Fatalf("%d: Expected compile error", i)
   810  			} else if !test.expectedCompileErr && err != nil {
   811  				t.Fatalf("%d: unexpected error when creating a new WebhookAuthorizer: %v", i, err)
   812  			}
   813  			if err == nil {
   814  				authorized, _, err := wh.Authorize(context.Background(), test.attr)
   815  				if test.expectedEvalErr && err == nil {
   816  					t.Fatalf("%d: Expected eval error", i)
   817  				} else if !test.expectedEvalErr && err != nil {
   818  					t.Fatalf("%d: unexpected error when authorizing: %v", i, err)
   819  				}
   820  
   821  				if test.expectedDecision != authorized {
   822  					t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedDecision, authorized)
   823  				}
   824  			}
   825  		})
   826  	}
   827  }
   828  
   829  func TestWebhookMetrics(t *testing.T) {
   830  	service := new(mockV1Service)
   831  	service.statusCode = 200
   832  	service.Allow()
   833  	s, err := NewV1TestServer(service, serverCert, serverKey, caCert)
   834  	if err != nil {
   835  		t.Fatal(err)
   836  	}
   837  	defer s.Close()
   838  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)
   839  
   840  	aliceAttr := authorizer.AttributesRecord{
   841  		User: &user.DefaultInfo{
   842  			Name: "alice",
   843  			UID:  "1",
   844  		},
   845  	}
   846  
   847  	testCases := []struct {
   848  		name         string
   849  		attr         authorizer.AttributesRecord
   850  		expressions1 []apiserver.WebhookMatchCondition
   851  		expressions2 []apiserver.WebhookMatchCondition
   852  		metrics      []string
   853  		want         string
   854  	}{
   855  		{
   856  			name: "should have one evaluation error from multiple failed match conditions",
   857  			attr: aliceAttr,
   858  			expressions1: []apiserver.WebhookMatchCondition{
   859  				{
   860  					Expression: "request.user == 'alice'",
   861  				},
   862  				{
   863  					Expression: "request.resourceAttributes.verb == 'get'",
   864  				},
   865  				{
   866  					Expression: "request.resourceAttributes.namespace == 'kittensandponies'",
   867  				},
   868  			},
   869  			expressions2: []apiserver.WebhookMatchCondition{
   870  				{
   871  					Expression: "request.user == 'alice'",
   872  				},
   873  			},
   874  			metrics: []string{
   875  				"apiserver_authorization_match_condition_evaluation_errors_total",
   876  			},
   877  			want: fmt.Sprintf(`
   878  					# HELP apiserver_authorization_match_condition_evaluation_errors_total [ALPHA] Total number of errors when an authorization webhook encounters a match condition error split by authorizer type and name.
   879  					# TYPE apiserver_authorization_match_condition_evaluation_errors_total counter
   880  					apiserver_authorization_match_condition_evaluation_errors_total{name="%s",type="%s"} 1
   881  					`, "wh1.example.com", "Webhook"),
   882  		},
   883  		{
   884  			name: "should have two webhook exclusions due to match condition",
   885  			attr: aliceAttr,
   886  			expressions1: []apiserver.WebhookMatchCondition{
   887  				{
   888  					Expression: "request.user == 'alice2'",
   889  				},
   890  				{
   891  					Expression: "request.uid == '1'",
   892  				},
   893  			},
   894  			expressions2: []apiserver.WebhookMatchCondition{
   895  				{
   896  					Expression: "request.user == 'alice1'",
   897  				},
   898  			},
   899  			metrics: []string{
   900  				"apiserver_authorization_match_condition_exclusions_total",
   901  			},
   902  			want: fmt.Sprintf(`
   903  					# HELP apiserver_authorization_match_condition_exclusions_total [ALPHA] Total number of exclusions when an authorization webhook is skipped because match conditions exclude it.
   904  					# TYPE apiserver_authorization_match_condition_exclusions_total counter
   905  					apiserver_authorization_match_condition_exclusions_total{name="%s",type="%s"} 1
   906  					apiserver_authorization_match_condition_exclusions_total{name="%s",type="%s"} 1
   907  					`, "wh1.example.com", "Webhook", "wh2.example.com", "Webhook"),
   908  		},
   909  	}
   910  
   911  	for _, tt := range testCases {
   912  		t.Run(tt.name, func(t *testing.T) {
   913  			celmetrics.ResetMetricsForTest()
   914  			defer celmetrics.ResetMetricsForTest()
   915  			wh1, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), tt.expressions1, "wh1.example.com")
   916  			if err != nil {
   917  				t.Fatal(err)
   918  			}
   919  			wh2, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, celAuthorizerMetrics(), tt.expressions2, "wh2.example.com")
   920  			if err != nil {
   921  				t.Fatal(err)
   922  			}
   923  			if err == nil {
   924  				_, _, _ = wh1.Authorize(context.Background(), tt.attr)
   925  				_, _, _ = wh2.Authorize(context.Background(), tt.attr)
   926  			}
   927  
   928  			if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil {
   929  				t.Fatal(err)
   930  			}
   931  		})
   932  	}
   933  }
   934  
   935  func BenchmarkNoCELExpressionFeatureOff(b *testing.B) {
   936  	expressions := []apiserver.WebhookMatchCondition{}
   937  	b.Run("compile", func(b *testing.B) {
   938  		benchmarkNewWebhookAuthorizer(b, expressions, false)
   939  	})
   940  	b.Run("authorize", func(b *testing.B) {
   941  		benchmarkWebhookAuthorize(b, expressions, false)
   942  	})
   943  }
   944  
   945  func BenchmarkNoCELExpressionFeatureOn(b *testing.B) {
   946  	expressions := []apiserver.WebhookMatchCondition{}
   947  	b.Run("compile", func(b *testing.B) {
   948  		benchmarkNewWebhookAuthorizer(b, expressions, true)
   949  	})
   950  	b.Run("authorize", func(b *testing.B) {
   951  		benchmarkWebhookAuthorize(b, expressions, true)
   952  	})
   953  }
   954  func BenchmarkWithOneCELExpressions(b *testing.B) {
   955  	expressions := []apiserver.WebhookMatchCondition{
   956  		{
   957  			Expression: "request.user == 'alice'",
   958  		},
   959  	}
   960  	b.Run("compile", func(b *testing.B) {
   961  		benchmarkNewWebhookAuthorizer(b, expressions, true)
   962  	})
   963  	b.Run("authorize", func(b *testing.B) {
   964  		benchmarkWebhookAuthorize(b, expressions, true)
   965  	})
   966  }
   967  func BenchmarkWithOneCELExpressionsFalse(b *testing.B) {
   968  	expressions := []apiserver.WebhookMatchCondition{
   969  		{
   970  			Expression: "request.user == 'alice2'",
   971  		},
   972  	}
   973  	b.Run("compile", func(b *testing.B) {
   974  		benchmarkNewWebhookAuthorizer(b, expressions, true)
   975  	})
   976  	b.Run("authorize", func(b *testing.B) {
   977  		benchmarkWebhookAuthorize(b, expressions, true)
   978  	})
   979  }
   980  func BenchmarkWithTwoCELExpressions(b *testing.B) {
   981  	expressions := []apiserver.WebhookMatchCondition{
   982  		{
   983  			Expression: "request.user == 'alice'",
   984  		},
   985  		{
   986  			Expression: "request.uid == '1'",
   987  		},
   988  	}
   989  	b.Run("compile", func(b *testing.B) {
   990  		benchmarkNewWebhookAuthorizer(b, expressions, true)
   991  	})
   992  	b.Run("authorize", func(b *testing.B) {
   993  		benchmarkWebhookAuthorize(b, expressions, true)
   994  	})
   995  }
   996  func BenchmarkWithTwoCELExpressionsFalse(b *testing.B) {
   997  	expressions := []apiserver.WebhookMatchCondition{
   998  		{
   999  			Expression: "request.user == 'alice'",
  1000  		},
  1001  		{
  1002  			Expression: "request.uid == '2'",
  1003  		},
  1004  	}
  1005  	b.Run("compile", func(b *testing.B) {
  1006  		benchmarkNewWebhookAuthorizer(b, expressions, true)
  1007  	})
  1008  	b.Run("authorize", func(b *testing.B) {
  1009  		benchmarkWebhookAuthorize(b, expressions, true)
  1010  	})
  1011  }
  1012  func BenchmarkWithManyCELExpressions(b *testing.B) {
  1013  	expressions := []apiserver.WebhookMatchCondition{
  1014  		{
  1015  			Expression: "request.user == 'alice'",
  1016  		},
  1017  		{
  1018  			Expression: "request.uid == '1'",
  1019  		},
  1020  		{
  1021  			Expression: "('group1' in request.groups)",
  1022  		},
  1023  		{
  1024  			Expression: "('key1' in request.extra)",
  1025  		},
  1026  		{
  1027  			Expression: "!('key2' in request.extra)",
  1028  		},
  1029  		{
  1030  			Expression: "('a' in request.extra['key1'])",
  1031  		},
  1032  		{
  1033  			Expression: "!('z' in request.extra['key1'])",
  1034  		},
  1035  		{
  1036  			Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
  1037  		},
  1038  	}
  1039  	b.Run("compile", func(b *testing.B) {
  1040  		benchmarkNewWebhookAuthorizer(b, expressions, true)
  1041  	})
  1042  	b.Run("authorize", func(b *testing.B) {
  1043  		benchmarkWebhookAuthorize(b, expressions, true)
  1044  	})
  1045  }
  1046  func BenchmarkWithManyCELExpressionsFalse(b *testing.B) {
  1047  	expressions := []apiserver.WebhookMatchCondition{
  1048  		{
  1049  			Expression: "request.user == 'alice'",
  1050  		},
  1051  		{
  1052  			Expression: "request.uid == '1'",
  1053  		},
  1054  		{
  1055  			Expression: "('group1' in request.groups)",
  1056  		},
  1057  		{
  1058  			Expression: "('key1' in request.extra)",
  1059  		},
  1060  		{
  1061  			Expression: "!('key2' in request.extra)",
  1062  		},
  1063  		{
  1064  			Expression: "('a' in request.extra['key1'])",
  1065  		},
  1066  		{
  1067  			Expression: "!('z' in request.extra['key1'])",
  1068  		},
  1069  		{
  1070  			Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies1'",
  1071  		},
  1072  	}
  1073  	b.Run("compile", func(b *testing.B) {
  1074  		benchmarkNewWebhookAuthorizer(b, expressions, true)
  1075  	})
  1076  	b.Run("authorize", func(b *testing.B) {
  1077  		benchmarkWebhookAuthorize(b, expressions, true)
  1078  	})
  1079  }
  1080  
  1081  func benchmarkNewWebhookAuthorizer(b *testing.B, expressions []apiserver.WebhookMatchCondition, featureEnabled bool) {
  1082  	service := new(mockV1Service)
  1083  	service.statusCode = 200
  1084  	service.Allow()
  1085  	s, err := NewV1TestServer(service, serverCert, serverKey, caCert)
  1086  	if err != nil {
  1087  		b.Fatal(err)
  1088  	}
  1089  	defer s.Close()
  1090  	featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, featureEnabled)
  1091  
  1092  	b.ResetTimer()
  1093  	for i := 0; i < b.N; i++ {
  1094  		// Create an authorizer with or without expressions to compile
  1095  		_, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), expressions, "")
  1096  		if err != nil {
  1097  			b.Fatal(err)
  1098  		}
  1099  	}
  1100  	b.StopTimer()
  1101  }
  1102  
  1103  func benchmarkWebhookAuthorize(b *testing.B, expressions []apiserver.WebhookMatchCondition, featureEnabled bool) {
  1104  	attr := authorizer.AttributesRecord{
  1105  		User: &user.DefaultInfo{
  1106  			Name:   "alice",
  1107  			UID:    "1",
  1108  			Groups: []string{"group1", "group2"},
  1109  			Extra:  map[string][]string{"key1": {"a", "b", "c"}},
  1110  		},
  1111  		ResourceRequest: true,
  1112  		Namespace:       "kittensandponies",
  1113  		Verb:            "get",
  1114  	}
  1115  	service := new(mockV1Service)
  1116  	service.statusCode = 200
  1117  	service.Allow()
  1118  	s, err := NewV1TestServer(service, serverCert, serverKey, caCert)
  1119  	if err != nil {
  1120  		b.Fatal(err)
  1121  	}
  1122  	defer s.Close()
  1123  	featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, featureEnabled)
  1124  	// Create an authorizer with or without expressions to compile
  1125  	wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), expressions, "")
  1126  	if err != nil {
  1127  		b.Fatal(err)
  1128  	}
  1129  
  1130  	b.ResetTimer()
  1131  	for i := 0; i < b.N; i++ {
  1132  		// Call authorize may or may not require cel evaluations
  1133  		_, _, err = wh.Authorize(context.Background(), attr)
  1134  		if err != nil {
  1135  			b.Fatal(err)
  1136  		}
  1137  	}
  1138  	b.StopTimer()
  1139  }
  1140  
  1141  // TestV1WebhookMatchConditions verifies cel expressions are compiled and evaluated correctly
  1142  func TestV1WebhookMatchConditions(t *testing.T) {
  1143  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)
  1144  	service := new(mockV1Service)
  1145  	service.statusCode = 200
  1146  	service.Allow()
  1147  	s, err := NewV1TestServer(service, serverCert, serverKey, caCert)
  1148  	if err != nil {
  1149  		t.Fatal(err)
  1150  	}
  1151  	defer s.Close()
  1152  
  1153  	aliceAttr := authorizer.AttributesRecord{
  1154  		User: &user.DefaultInfo{
  1155  			Name:   "alice",
  1156  			UID:    "1",
  1157  			Groups: []string{"group1", "group2"},
  1158  			Extra:  map[string][]string{"key1": {"a", "b", "c"}},
  1159  		},
  1160  		ResourceRequest: true,
  1161  		Namespace:       "kittensandponies",
  1162  		Verb:            "get",
  1163  	}
  1164  	bobAttr := authorizer.AttributesRecord{
  1165  		User: &user.DefaultInfo{
  1166  			Name: "bob",
  1167  		},
  1168  		ResourceRequest: false,
  1169  		Namespace:       "kittensandponies",
  1170  		Verb:            "get",
  1171  	}
  1172  	alice2Attr := authorizer.AttributesRecord{
  1173  		User: &user.DefaultInfo{
  1174  			Name: "alice2",
  1175  		},
  1176  	}
  1177  	type webhookMatchConditionsTestCase struct {
  1178  		name               string
  1179  		attr               authorizer.AttributesRecord
  1180  		expectedCompileErr string
  1181  		expectedEvalErr    string
  1182  		expectedDecision   authorizer.Decision
  1183  		expressions        []apiserver.WebhookMatchCondition
  1184  	}
  1185  
  1186  	tests := []webhookMatchConditionsTestCase{
  1187  		{
  1188  			name:               "match all with no expressions",
  1189  			attr:               aliceAttr,
  1190  			expectedCompileErr: "",
  1191  			expectedDecision:   authorizer.DecisionAllow,
  1192  			expressions:        []apiserver.WebhookMatchCondition{},
  1193  		},
  1194  		{
  1195  			name:               "match all against all expressions",
  1196  			attr:               aliceAttr,
  1197  			expectedCompileErr: "",
  1198  			expectedDecision:   authorizer.DecisionAllow,
  1199  			expressions: []apiserver.WebhookMatchCondition{
  1200  				{
  1201  					Expression: "request.user == 'alice'",
  1202  				},
  1203  				{
  1204  					Expression: "request.uid == '1'",
  1205  				},
  1206  				{
  1207  					Expression: "('group1' in request.groups)",
  1208  				},
  1209  				{
  1210  					Expression: "('key1' in request.extra)",
  1211  				},
  1212  				{
  1213  					Expression: "!('key2' in request.extra)",
  1214  				},
  1215  				{
  1216  					Expression: "('a' in request.extra['key1'])",
  1217  				},
  1218  				{
  1219  					Expression: "!('z' in request.extra['key1'])",
  1220  				},
  1221  				{
  1222  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
  1223  				},
  1224  			},
  1225  		},
  1226  		{
  1227  			name:               "match all except group, eval to one successful false, no error",
  1228  			attr:               aliceAttr,
  1229  			expectedCompileErr: "",
  1230  			expectedDecision:   authorizer.DecisionNoOpinion,
  1231  			expectedEvalErr:    "",
  1232  			expressions: []apiserver.WebhookMatchCondition{
  1233  				{
  1234  					Expression: "request.user == 'alice'",
  1235  				},
  1236  				{
  1237  					Expression: "request.uid == '1'",
  1238  				},
  1239  				{
  1240  					Expression: "('group3' in request.groups)",
  1241  				},
  1242  				{
  1243  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
  1244  				},
  1245  			},
  1246  		},
  1247  		{
  1248  			name:               "match condition with one compilation error",
  1249  			attr:               aliceAttr,
  1250  			expectedCompileErr: "matchConditions[2].expression: Invalid value: \"('group3' in request.group)\": compilation failed: ERROR: <input>:1:21: undefined field 'group'\n | ('group3' in request.group)\n | ....................^",
  1251  			expectedDecision:   authorizer.DecisionNoOpinion,
  1252  			expressions: []apiserver.WebhookMatchCondition{
  1253  				{
  1254  					Expression: "request.user == 'alice'",
  1255  				},
  1256  				{
  1257  					Expression: "request.uid == '1'",
  1258  				},
  1259  				{
  1260  					Expression: "('group3' in request.group)",
  1261  				},
  1262  				{
  1263  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
  1264  				},
  1265  			},
  1266  		},
  1267  		{
  1268  			name:               "match all except uid",
  1269  			attr:               aliceAttr,
  1270  			expectedCompileErr: "",
  1271  			expectedDecision:   authorizer.DecisionNoOpinion,
  1272  			expressions: []apiserver.WebhookMatchCondition{
  1273  				{
  1274  					Expression: "request.user == 'alice'",
  1275  				},
  1276  				{
  1277  					Expression: "request.uid == '2'",
  1278  				},
  1279  				{
  1280  					Expression: "('group1' in request.groups)",
  1281  				},
  1282  				{
  1283  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
  1284  				},
  1285  			},
  1286  		},
  1287  		{
  1288  			name:               "match on user name but not namespace",
  1289  			attr:               aliceAttr,
  1290  			expectedCompileErr: "",
  1291  			expectedDecision:   authorizer.DecisionNoOpinion,
  1292  			expressions: []apiserver.WebhookMatchCondition{
  1293  				{
  1294  					Expression: "request.user == 'alice'",
  1295  				},
  1296  				{
  1297  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
  1298  				},
  1299  			},
  1300  		},
  1301  		{
  1302  			name:               "mismatch on user name",
  1303  			attr:               bobAttr,
  1304  			expectedCompileErr: "",
  1305  			expectedDecision:   authorizer.DecisionNoOpinion,
  1306  			expressions: []apiserver.WebhookMatchCondition{
  1307  				{
  1308  					Expression: "request.user == 'alice'",
  1309  				},
  1310  			},
  1311  		},
  1312  		{
  1313  			name:               "match on user name but not resourceAttributes",
  1314  			attr:               bobAttr,
  1315  			expectedCompileErr: "",
  1316  			expectedDecision:   authorizer.DecisionNoOpinion,
  1317  			expressions: []apiserver.WebhookMatchCondition{
  1318  				{
  1319  					Expression: "request.user == 'bob'",
  1320  				},
  1321  				{
  1322  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
  1323  				},
  1324  			},
  1325  		},
  1326  		{
  1327  			name:               "expression failed to compile due to wrong return type",
  1328  			attr:               bobAttr,
  1329  			expectedCompileErr: `matchConditions[0].expression: Invalid value: "request.user": must evaluate to bool but got string`,
  1330  			expectedDecision:   authorizer.DecisionNoOpinion,
  1331  			expressions: []apiserver.WebhookMatchCondition{
  1332  				{
  1333  					Expression: "request.user",
  1334  				},
  1335  			},
  1336  		},
  1337  		{
  1338  			name:               "eval failed due to errors, no successful fail",
  1339  			attr:               alice2Attr,
  1340  			expectedCompileErr: "",
  1341  			expectedEvalErr:    "cel evaluation error: expression 'request.resourceAttributes.namespace == 'kittensandponies'' resulted in error: no such key: resourceAttributes",
  1342  			expectedDecision:   authorizer.DecisionNoOpinion,
  1343  			expressions: []apiserver.WebhookMatchCondition{
  1344  				{
  1345  					Expression: "request.user == 'alice2'",
  1346  				},
  1347  				{
  1348  					Expression: "request.resourceAttributes.namespace == 'kittensandponies'",
  1349  				},
  1350  			},
  1351  		},
  1352  		{
  1353  			name:               "at least one matchCondition successfully evaluates to FALSE, error ignored",
  1354  			attr:               alice2Attr,
  1355  			expectedCompileErr: "",
  1356  			expectedEvalErr:    "",
  1357  			expectedDecision:   authorizer.DecisionNoOpinion,
  1358  			expressions: []apiserver.WebhookMatchCondition{
  1359  				{
  1360  					Expression: "request.user != 'alice2'",
  1361  				},
  1362  				{
  1363  					Expression: "request.resourceAttributes.namespace == 'kittensandponies'",
  1364  				},
  1365  			},
  1366  		},
  1367  		{
  1368  			name:               "match on user name but failed to compile due to type check in nonResourceAttributes",
  1369  			attr:               bobAttr,
  1370  			expectedCompileErr: "matchConditions[1].expression: Invalid value: \"request.nonResourceAttributes.verb == 2\": compilation failed: ERROR: <input>:1:36: found no matching overload for '_==_' applied to '(string, int)'\n | request.nonResourceAttributes.verb == 2\n | ...................................^",
  1371  			expectedDecision:   authorizer.DecisionNoOpinion,
  1372  			expressions: []apiserver.WebhookMatchCondition{
  1373  				{
  1374  					Expression: "request.user == 'bob'",
  1375  				},
  1376  				{
  1377  					Expression: "request.nonResourceAttributes.verb == 2",
  1378  				},
  1379  			},
  1380  		},
  1381  		{
  1382  			name:               "match on user name and nonresourceAttributes",
  1383  			attr:               bobAttr,
  1384  			expectedCompileErr: "",
  1385  			expectedDecision:   authorizer.DecisionAllow,
  1386  			expressions: []apiserver.WebhookMatchCondition{
  1387  				{
  1388  					Expression: "request.user == 'bob'",
  1389  				},
  1390  				{
  1391  					Expression: "has(request.nonResourceAttributes) && request.nonResourceAttributes.verb == 'get'",
  1392  				},
  1393  			},
  1394  		},
  1395  		{
  1396  			name:               "match eval failed with bad SubjectAccessReviewSpec",
  1397  			attr:               authorizer.AttributesRecord{},
  1398  			expectedCompileErr: "",
  1399  			// default decisionOnError in newWithBackoff to skip
  1400  			expectedDecision: authorizer.DecisionNoOpinion,
  1401  			expectedEvalErr:  "cel evaluation error: expression 'request.resourceAttributes.verb == 'get'' resulted in error: no such key: resourceAttributes",
  1402  			expressions: []apiserver.WebhookMatchCondition{
  1403  				{
  1404  					Expression: "request.resourceAttributes.verb == 'get'",
  1405  				},
  1406  			},
  1407  		},
  1408  	}
  1409  
  1410  	for i, test := range tests {
  1411  		t.Run(test.name, func(t *testing.T) {
  1412  			wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions, "")
  1413  			if len(test.expectedCompileErr) > 0 && err == nil {
  1414  				t.Fatalf("%d: Expected compile error", i)
  1415  			} else if len(test.expectedCompileErr) == 0 && err != nil {
  1416  				t.Fatalf("%d: unexpected error when creating a new WebhookAuthorizer: %v", i, err)
  1417  			}
  1418  			if err != nil {
  1419  				if d := cmp.Diff(test.expectedCompileErr, err.Error()); d != "" {
  1420  					t.Fatalf("newV1Authorizer mismatch (-want +got):\n%s", d)
  1421  				}
  1422  			}
  1423  			if err == nil {
  1424  				authorized, _, err := wh.Authorize(context.Background(), test.attr)
  1425  				if len(test.expectedEvalErr) > 0 && err == nil {
  1426  					t.Fatalf("%d: Expected eval error", i)
  1427  				} else if len(test.expectedEvalErr) == 0 && err != nil {
  1428  					t.Fatalf("%d: unexpected error when authorizing: %v", i, err)
  1429  				}
  1430  
  1431  				if err != nil {
  1432  					if d := cmp.Diff(test.expectedEvalErr, err.Error()); d != "" {
  1433  						t.Fatalf("Authorize mismatch (-want +got):\n%s", d)
  1434  					}
  1435  				}
  1436  
  1437  				if test.expectedDecision != authorized {
  1438  					t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedDecision, authorized)
  1439  				}
  1440  			}
  1441  		})
  1442  	}
  1443  }
  1444  
  1445  func noopAuthorizerMetrics() metrics.AuthorizerMetrics {
  1446  	return metrics.NoopAuthorizerMetrics{}
  1447  }
  1448  
  1449  func celAuthorizerMetrics() metrics.AuthorizerMetrics {
  1450  	return celAuthorizerMetricsType{
  1451  		MatcherMetrics: celmetrics.NewMatcherMetrics(),
  1452  	}
  1453  }
  1454  
  1455  type celAuthorizerMetricsType struct {
  1456  	metrics.NoopRequestMetrics
  1457  	metrics.NoopWebhookMetrics
  1458  	celmetrics.MatcherMetrics
  1459  }