k8s.io/apiserver@v0.31.1/pkg/apis/apiserver/validation/validation_test.go (about)

     1  /*
     2  Copyright 2023 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 validation
    18  
    19  import (
    20  	"crypto/ecdsa"
    21  	"crypto/elliptic"
    22  	"crypto/rand"
    23  	"encoding/pem"
    24  	"fmt"
    25  	"os"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/util/errors"
    33  	"k8s.io/apimachinery/pkg/util/sets"
    34  	"k8s.io/apimachinery/pkg/util/validation/field"
    35  	api "k8s.io/apiserver/pkg/apis/apiserver"
    36  	authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
    37  	"k8s.io/apiserver/pkg/cel/environment"
    38  	"k8s.io/apiserver/pkg/features"
    39  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    40  	certutil "k8s.io/client-go/util/cert"
    41  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    42  	"k8s.io/utils/pointer"
    43  )
    44  
    45  var (
    46  	compiler = authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
    47  )
    48  
    49  func TestValidateAuthenticationConfiguration(t *testing.T) {
    50  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)
    51  
    52  	testCases := []struct {
    53  		name              string
    54  		in                *api.AuthenticationConfiguration
    55  		disallowedIssuers []string
    56  		want              string
    57  	}{
    58  		{
    59  			name: "jwt authenticator is empty",
    60  			in:   &api.AuthenticationConfiguration{},
    61  			want: "",
    62  		},
    63  		{
    64  			name: "duplicate issuer across jwt authenticators",
    65  			in: &api.AuthenticationConfiguration{
    66  				JWT: []api.JWTAuthenticator{
    67  					{
    68  						Issuer: api.Issuer{
    69  							URL:       "https://issuer-url",
    70  							Audiences: []string{"audience"},
    71  						},
    72  						ClaimValidationRules: []api.ClaimValidationRule{
    73  							{
    74  								Claim:         "foo",
    75  								RequiredValue: "bar",
    76  							},
    77  						},
    78  						ClaimMappings: api.ClaimMappings{
    79  							Username: api.PrefixedClaimOrExpression{
    80  								Claim:  "sub",
    81  								Prefix: pointer.String("prefix"),
    82  							},
    83  						},
    84  					},
    85  					{
    86  						Issuer: api.Issuer{
    87  							URL:       "https://issuer-url",
    88  							Audiences: []string{"audience"},
    89  						},
    90  						ClaimValidationRules: []api.ClaimValidationRule{
    91  							{
    92  								Claim:         "foo",
    93  								RequiredValue: "bar",
    94  							},
    95  						},
    96  						ClaimMappings: api.ClaimMappings{
    97  							Username: api.PrefixedClaimOrExpression{
    98  								Claim:  "sub",
    99  								Prefix: pointer.String("prefix"),
   100  							},
   101  						},
   102  					},
   103  				},
   104  			},
   105  			want: `jwt[1].issuer.url: Duplicate value: "https://issuer-url"`,
   106  		},
   107  		{
   108  			name: "duplicate discoveryURL across jwt authenticators",
   109  			in: &api.AuthenticationConfiguration{
   110  				JWT: []api.JWTAuthenticator{
   111  					{
   112  						Issuer: api.Issuer{
   113  							URL:          "https://issuer-url",
   114  							DiscoveryURL: "https://discovery-url/.well-known/openid-configuration",
   115  							Audiences:    []string{"audience"},
   116  						},
   117  						ClaimValidationRules: []api.ClaimValidationRule{
   118  							{
   119  								Claim:         "foo",
   120  								RequiredValue: "bar",
   121  							},
   122  						},
   123  						ClaimMappings: api.ClaimMappings{
   124  							Username: api.PrefixedClaimOrExpression{
   125  								Claim:  "sub",
   126  								Prefix: pointer.String("prefix"),
   127  							},
   128  						},
   129  					},
   130  					{
   131  						Issuer: api.Issuer{
   132  							URL:          "https://different-issuer-url",
   133  							DiscoveryURL: "https://discovery-url/.well-known/openid-configuration",
   134  							Audiences:    []string{"audience"},
   135  						},
   136  						ClaimValidationRules: []api.ClaimValidationRule{
   137  							{
   138  								Claim:         "foo",
   139  								RequiredValue: "bar",
   140  							},
   141  						},
   142  						ClaimMappings: api.ClaimMappings{
   143  							Username: api.PrefixedClaimOrExpression{
   144  								Claim:  "sub",
   145  								Prefix: pointer.String("prefix"),
   146  							},
   147  						},
   148  					},
   149  				},
   150  			},
   151  			want: `jwt[1].issuer.discoveryURL: Duplicate value: "https://discovery-url/.well-known/openid-configuration"`,
   152  		},
   153  		{
   154  			name: "failed issuer validation",
   155  			in: &api.AuthenticationConfiguration{
   156  				JWT: []api.JWTAuthenticator{
   157  					{
   158  						Issuer: api.Issuer{
   159  							URL:       "invalid-url",
   160  							Audiences: []string{"audience"},
   161  						},
   162  						ClaimMappings: api.ClaimMappings{
   163  							Username: api.PrefixedClaimOrExpression{
   164  								Claim:  "claim",
   165  								Prefix: pointer.String("prefix"),
   166  							},
   167  						},
   168  					},
   169  				},
   170  			},
   171  			want: `jwt[0].issuer.url: Invalid value: "invalid-url": URL scheme must be https`,
   172  		},
   173  		{
   174  			name: "failed claimValidationRule validation",
   175  			in: &api.AuthenticationConfiguration{
   176  				JWT: []api.JWTAuthenticator{
   177  					{
   178  						Issuer: api.Issuer{
   179  							URL:       "https://issuer-url",
   180  							Audiences: []string{"audience"},
   181  						},
   182  						ClaimValidationRules: []api.ClaimValidationRule{
   183  							{
   184  								Claim:         "foo",
   185  								RequiredValue: "bar",
   186  							},
   187  							{
   188  								Claim:         "foo",
   189  								RequiredValue: "baz",
   190  							},
   191  						},
   192  						ClaimMappings: api.ClaimMappings{
   193  							Username: api.PrefixedClaimOrExpression{
   194  								Claim:  "claim",
   195  								Prefix: pointer.String("prefix"),
   196  							},
   197  						},
   198  					},
   199  				},
   200  			},
   201  			want: `jwt[0].claimValidationRules[1].claim: Duplicate value: "foo"`,
   202  		},
   203  		{
   204  			name: "failed claimMapping validation",
   205  			in: &api.AuthenticationConfiguration{
   206  				JWT: []api.JWTAuthenticator{
   207  					{
   208  						Issuer: api.Issuer{
   209  							URL:       "https://issuer-url",
   210  							Audiences: []string{"audience"},
   211  						},
   212  						ClaimValidationRules: []api.ClaimValidationRule{
   213  							{
   214  								Claim:         "foo",
   215  								RequiredValue: "bar",
   216  							},
   217  						},
   218  						ClaimMappings: api.ClaimMappings{
   219  							Username: api.PrefixedClaimOrExpression{
   220  								Prefix: pointer.String("prefix"),
   221  							},
   222  						},
   223  					},
   224  				},
   225  			},
   226  			want: "jwt[0].claimMappings.username: Required value: claim or expression is required",
   227  		},
   228  		{
   229  			name: "failed userValidationRule validation",
   230  			in: &api.AuthenticationConfiguration{
   231  				JWT: []api.JWTAuthenticator{
   232  					{
   233  						Issuer: api.Issuer{
   234  							URL:       "https://issuer-url",
   235  							Audiences: []string{"audience"},
   236  						},
   237  						ClaimValidationRules: []api.ClaimValidationRule{
   238  							{
   239  								Claim:         "foo",
   240  								RequiredValue: "bar",
   241  							},
   242  						},
   243  						ClaimMappings: api.ClaimMappings{
   244  							Username: api.PrefixedClaimOrExpression{
   245  								Claim:  "sub",
   246  								Prefix: pointer.String("prefix"),
   247  							},
   248  						},
   249  						UserValidationRules: []api.UserValidationRule{
   250  							{Expression: "user.username == 'foo'"},
   251  							{Expression: "user.username == 'foo'"},
   252  						},
   253  					},
   254  				},
   255  			},
   256  			want: `jwt[0].userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`,
   257  		},
   258  		{
   259  			name: "valid authentication configuration with disallowed issuer",
   260  			in: &api.AuthenticationConfiguration{
   261  				JWT: []api.JWTAuthenticator{
   262  					{
   263  						Issuer: api.Issuer{
   264  							URL:       "https://issuer-url",
   265  							Audiences: []string{"audience"},
   266  						},
   267  						ClaimValidationRules: []api.ClaimValidationRule{
   268  							{
   269  								Claim:         "foo",
   270  								RequiredValue: "bar",
   271  							},
   272  						},
   273  						ClaimMappings: api.ClaimMappings{
   274  							Username: api.PrefixedClaimOrExpression{
   275  								Claim:  "sub",
   276  								Prefix: pointer.String("prefix"),
   277  							},
   278  						},
   279  					},
   280  				},
   281  			},
   282  			disallowedIssuers: []string{"a", "b", "https://issuer-url", "c"},
   283  			want:              `jwt[0].issuer.url: Invalid value: "https://issuer-url": URL must not overlap with disallowed issuers: [a b c https://issuer-url]`,
   284  		},
   285  		{
   286  			name: "valid authentication configuration that uses unverified email",
   287  			in: &api.AuthenticationConfiguration{
   288  				JWT: []api.JWTAuthenticator{
   289  					{
   290  						Issuer: api.Issuer{
   291  							URL:       "https://issuer-url",
   292  							Audiences: []string{"audience"},
   293  						},
   294  						ClaimValidationRules: []api.ClaimValidationRule{
   295  							{
   296  								Claim:         "foo",
   297  								RequiredValue: "bar",
   298  							},
   299  						},
   300  						ClaimMappings: api.ClaimMappings{
   301  							Username: api.PrefixedClaimOrExpression{
   302  								Expression: "claims.email",
   303  							},
   304  						},
   305  					},
   306  				},
   307  			},
   308  			want: `jwt[0].claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
   309  		},
   310  		{
   311  			name: "valid authentication configuration that almost uses unverified email",
   312  			in: &api.AuthenticationConfiguration{
   313  				JWT: []api.JWTAuthenticator{
   314  					{
   315  						Issuer: api.Issuer{
   316  							URL:       "https://issuer-url",
   317  							Audiences: []string{"audience"},
   318  						},
   319  						ClaimValidationRules: []api.ClaimValidationRule{
   320  							{
   321  								Claim:         "foo",
   322  								RequiredValue: "bar",
   323  							},
   324  						},
   325  						ClaimMappings: api.ClaimMappings{
   326  							Username: api.PrefixedClaimOrExpression{
   327  								Expression: "claims.email_",
   328  							},
   329  						},
   330  					},
   331  				},
   332  			},
   333  			want: "",
   334  		},
   335  		{
   336  			name: "valid authentication configuration that uses unverified email join",
   337  			in: &api.AuthenticationConfiguration{
   338  				JWT: []api.JWTAuthenticator{
   339  					{
   340  						Issuer: api.Issuer{
   341  							URL:       "https://issuer-url",
   342  							Audiences: []string{"audience"},
   343  						},
   344  						ClaimValidationRules: []api.ClaimValidationRule{
   345  							{
   346  								Claim:         "foo",
   347  								RequiredValue: "bar",
   348  							},
   349  						},
   350  						ClaimMappings: api.ClaimMappings{
   351  							Username: api.PrefixedClaimOrExpression{
   352  								Expression: `['yay', string(claims.email), 'panda'].join(' ')`,
   353  							},
   354  						},
   355  					},
   356  				},
   357  			},
   358  			want: `jwt[0].claimMappings.username.expression: Invalid value: "['yay', string(claims.email), 'panda'].join(' ')": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
   359  		},
   360  		{
   361  			name: "valid authentication configuration that uses unverified optional email",
   362  			in: &api.AuthenticationConfiguration{
   363  				JWT: []api.JWTAuthenticator{
   364  					{
   365  						Issuer: api.Issuer{
   366  							URL:       "https://issuer-url",
   367  							Audiences: []string{"audience"},
   368  						},
   369  						ClaimValidationRules: []api.ClaimValidationRule{
   370  							{
   371  								Claim:         "foo",
   372  								RequiredValue: "bar",
   373  							},
   374  						},
   375  						ClaimMappings: api.ClaimMappings{
   376  							Username: api.PrefixedClaimOrExpression{
   377  								Expression: `claims.?email`,
   378  							},
   379  						},
   380  					},
   381  				},
   382  			},
   383  			want: `jwt[0].claimMappings.username.expression: Invalid value: "claims.?email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
   384  		},
   385  		{
   386  			name: "valid authentication configuration that uses unverified optional map email key",
   387  			in: &api.AuthenticationConfiguration{
   388  				JWT: []api.JWTAuthenticator{
   389  					{
   390  						Issuer: api.Issuer{
   391  							URL:       "https://issuer-url",
   392  							Audiences: []string{"audience"},
   393  						},
   394  						ClaimValidationRules: []api.ClaimValidationRule{
   395  							{
   396  								Claim:         "foo",
   397  								RequiredValue: "bar",
   398  							},
   399  						},
   400  						ClaimMappings: api.ClaimMappings{
   401  							Username: api.PrefixedClaimOrExpression{
   402  								Expression: `{claims.?email: "panda"}`,
   403  							},
   404  						},
   405  					},
   406  				},
   407  			},
   408  			want: `jwt[0].claimMappings.username.expression: Invalid value: "{claims.?email: \"panda\"}": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
   409  		},
   410  		{
   411  			name: "valid authentication configuration that uses unverified optional map email value",
   412  			in: &api.AuthenticationConfiguration{
   413  				JWT: []api.JWTAuthenticator{
   414  					{
   415  						Issuer: api.Issuer{
   416  							URL:       "https://issuer-url",
   417  							Audiences: []string{"audience"},
   418  						},
   419  						ClaimValidationRules: []api.ClaimValidationRule{
   420  							{
   421  								Claim:         "foo",
   422  								RequiredValue: "bar",
   423  							},
   424  						},
   425  						ClaimMappings: api.ClaimMappings{
   426  							Username: api.PrefixedClaimOrExpression{
   427  								Expression: `{"fancy": claims.?email}`,
   428  							},
   429  						},
   430  					},
   431  				},
   432  			},
   433  			want: `jwt[0].claimMappings.username.expression: Invalid value: "{\"fancy\": claims.?email}": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
   434  		},
   435  		{
   436  			name: "valid authentication configuration that uses unverified email value in list iteration",
   437  			in: &api.AuthenticationConfiguration{
   438  				JWT: []api.JWTAuthenticator{
   439  					{
   440  						Issuer: api.Issuer{
   441  							URL:       "https://issuer-url",
   442  							Audiences: []string{"audience"},
   443  						},
   444  						ClaimValidationRules: []api.ClaimValidationRule{
   445  							{
   446  								Claim:         "foo",
   447  								RequiredValue: "bar",
   448  							},
   449  						},
   450  						ClaimMappings: api.ClaimMappings{
   451  							Username: api.PrefixedClaimOrExpression{
   452  								Expression: `["a"].map(i, i + claims.email)`,
   453  							},
   454  						},
   455  					},
   456  				},
   457  			},
   458  			want: `jwt[0].claimMappings.username.expression: Invalid value: "[\"a\"].map(i, i + claims.email)": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
   459  		},
   460  		{
   461  			name: "valid authentication configuration that uses verified email join via rule",
   462  			in: &api.AuthenticationConfiguration{
   463  				JWT: []api.JWTAuthenticator{
   464  					{
   465  						Issuer: api.Issuer{
   466  							URL:       "https://issuer-url",
   467  							Audiences: []string{"audience"},
   468  						},
   469  						ClaimValidationRules: []api.ClaimValidationRule{
   470  							{
   471  								Expression: `string(claims.email_verified) == "panda"`,
   472  							},
   473  						},
   474  						ClaimMappings: api.ClaimMappings{
   475  							Username: api.PrefixedClaimOrExpression{
   476  								Expression: `['yay', string(claims.email), 'panda'].join(' ')`,
   477  							},
   478  						},
   479  					},
   480  				},
   481  			},
   482  			want: "",
   483  		},
   484  		{
   485  			name: "valid authentication configuration that uses verified email join via extra",
   486  			in: &api.AuthenticationConfiguration{
   487  				JWT: []api.JWTAuthenticator{
   488  					{
   489  						Issuer: api.Issuer{
   490  							URL:       "https://issuer-url",
   491  							Audiences: []string{"audience"},
   492  						},
   493  						ClaimValidationRules: []api.ClaimValidationRule{
   494  							{
   495  								Claim:         "foo",
   496  								RequiredValue: "bar",
   497  							},
   498  						},
   499  						ClaimMappings: api.ClaimMappings{
   500  							Username: api.PrefixedClaimOrExpression{
   501  								Expression: `['yay', string(claims.email), 'panda'].join(' ')`,
   502  							},
   503  							Extra: []api.ExtraMapping{
   504  								{Key: "panda.io/foo", ValueExpression: "claims.email_verified.upperAscii()"},
   505  							},
   506  						},
   507  					},
   508  				},
   509  			},
   510  			want: "",
   511  		},
   512  		{
   513  			name: "valid authentication configuration that uses verified email join via extra optional",
   514  			in: &api.AuthenticationConfiguration{
   515  				JWT: []api.JWTAuthenticator{
   516  					{
   517  						Issuer: api.Issuer{
   518  							URL:       "https://issuer-url",
   519  							Audiences: []string{"audience"},
   520  						},
   521  						ClaimValidationRules: []api.ClaimValidationRule{
   522  							{
   523  								Claim:         "foo",
   524  								RequiredValue: "bar",
   525  							},
   526  						},
   527  						ClaimMappings: api.ClaimMappings{
   528  							Username: api.PrefixedClaimOrExpression{
   529  								Expression: `['yay', string(claims.email), 'panda'].join(' ')`,
   530  							},
   531  							Extra: []api.ExtraMapping{
   532  								{Key: "panda.io/foo", ValueExpression: "claims.?email_verified"},
   533  							},
   534  						},
   535  					},
   536  				},
   537  			},
   538  			want: "",
   539  		},
   540  		{
   541  			name: "valid authentication configuration that uses email and email_verified || true via username",
   542  			in: &api.AuthenticationConfiguration{
   543  				JWT: []api.JWTAuthenticator{
   544  					{
   545  						Issuer: api.Issuer{
   546  							URL:       "https://issuer-url",
   547  							Audiences: []string{"audience"},
   548  						},
   549  						ClaimValidationRules: []api.ClaimValidationRule{
   550  							{
   551  								Claim:         "foo",
   552  								RequiredValue: "bar",
   553  							},
   554  						},
   555  						// allow email claim when email_verified is true or absent
   556  						ClaimMappings: api.ClaimMappings{
   557  							Username: api.PrefixedClaimOrExpression{
   558  								Expression: `claims.?email_verified.orValue(true) ? claims.email : claims.sub`,
   559  							},
   560  						},
   561  					},
   562  				},
   563  			},
   564  			want: "",
   565  		},
   566  		{
   567  			name: "valid authentication configuration that uses email and email_verified || false via username",
   568  			in: &api.AuthenticationConfiguration{
   569  				JWT: []api.JWTAuthenticator{
   570  					{
   571  						Issuer: api.Issuer{
   572  							URL:       "https://issuer-url",
   573  							Audiences: []string{"audience"},
   574  						},
   575  						ClaimValidationRules: []api.ClaimValidationRule{
   576  							{
   577  								Claim:         "foo",
   578  								RequiredValue: "bar",
   579  							},
   580  						},
   581  						// allow email claim only when email_verified is present and true
   582  						ClaimMappings: api.ClaimMappings{
   583  							Username: api.PrefixedClaimOrExpression{
   584  								Expression: `claims.?email_verified.orValue(false) ? claims.email : claims.sub`,
   585  							},
   586  						},
   587  					},
   588  				},
   589  			},
   590  			want: "",
   591  		},
   592  		{
   593  			name: "valid authentication configuration",
   594  			in: &api.AuthenticationConfiguration{
   595  				JWT: []api.JWTAuthenticator{
   596  					{
   597  						Issuer: api.Issuer{
   598  							URL:       "https://issuer-url",
   599  							Audiences: []string{"audience"},
   600  						},
   601  						ClaimValidationRules: []api.ClaimValidationRule{
   602  							{
   603  								Claim:         "foo",
   604  								RequiredValue: "bar",
   605  							},
   606  						},
   607  						ClaimMappings: api.ClaimMappings{
   608  							Username: api.PrefixedClaimOrExpression{
   609  								Claim:  "sub",
   610  								Prefix: pointer.String("prefix"),
   611  							},
   612  						},
   613  					},
   614  				},
   615  			},
   616  			want: "",
   617  		},
   618  	}
   619  
   620  	for _, tt := range testCases {
   621  		t.Run(tt.name, func(t *testing.T) {
   622  			got := ValidateAuthenticationConfiguration(tt.in, tt.disallowedIssuers).ToAggregate()
   623  			if d := cmp.Diff(tt.want, errString(got)); d != "" {
   624  				t.Fatalf("AuthenticationConfiguration validation mismatch (-want +got):\n%s", d)
   625  			}
   626  		})
   627  	}
   628  }
   629  
   630  func TestValidateIssuerURL(t *testing.T) {
   631  	fldPath := field.NewPath("issuer", "url")
   632  
   633  	testCases := []struct {
   634  		name              string
   635  		in                string
   636  		disallowedIssuers sets.Set[string]
   637  		want              string
   638  	}{
   639  		{
   640  			name: "url is empty",
   641  			in:   "",
   642  			want: "issuer.url: Required value: URL is required",
   643  		},
   644  		{
   645  			name: "url parse error",
   646  			in:   "https://issuer-url:invalid-port",
   647  			want: `issuer.url: Invalid value: "https://issuer-url:invalid-port": parse "https://issuer-url:invalid-port": invalid port ":invalid-port" after host`,
   648  		},
   649  		{
   650  			name: "url is not https",
   651  			in:   "http://issuer-url",
   652  			want: `issuer.url: Invalid value: "http://issuer-url": URL scheme must be https`,
   653  		},
   654  		{
   655  			name: "url user info is not allowed",
   656  			in:   "https://user:pass@issuer-url",
   657  			want: `issuer.url: Invalid value: "https://user:pass@issuer-url": URL must not contain a username or password`,
   658  		},
   659  		{
   660  			name: "url raw query is not allowed",
   661  			in:   "https://issuer-url?query",
   662  			want: `issuer.url: Invalid value: "https://issuer-url?query": URL must not contain a query`,
   663  		},
   664  		{
   665  			name: "url fragment is not allowed",
   666  			in:   "https://issuer-url#fragment",
   667  			want: `issuer.url: Invalid value: "https://issuer-url#fragment": URL must not contain a fragment`,
   668  		},
   669  		{
   670  			name:              "valid url that is disallowed",
   671  			in:                "https://issuer-url",
   672  			disallowedIssuers: sets.New("https://issuer-url"),
   673  			want:              `issuer.url: Invalid value: "https://issuer-url": URL must not overlap with disallowed issuers: [https://issuer-url]`,
   674  		},
   675  		{
   676  			name: "valid url",
   677  			in:   "https://issuer-url",
   678  			want: "",
   679  		},
   680  	}
   681  
   682  	for _, tt := range testCases {
   683  		t.Run(tt.name, func(t *testing.T) {
   684  			got := validateIssuerURL(tt.in, tt.disallowedIssuers, fldPath).ToAggregate()
   685  			if d := cmp.Diff(tt.want, errString(got)); d != "" {
   686  				t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
   687  			}
   688  		})
   689  	}
   690  }
   691  
   692  func TestValidateIssuerDiscoveryURL(t *testing.T) {
   693  	fldPath := field.NewPath("issuer", "discoveryURL")
   694  
   695  	testCases := []struct {
   696  		name      string
   697  		in        string
   698  		issuerURL string
   699  		want      string
   700  	}{
   701  		{
   702  			name: "url is empty",
   703  			in:   "",
   704  			want: "",
   705  		},
   706  		{
   707  			name: "url parse error",
   708  			in:   "https://oidc.oidc-namespace.svc:invalid-port",
   709  			want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc:invalid-port": parse "https://oidc.oidc-namespace.svc:invalid-port": invalid port ":invalid-port" after host`,
   710  		},
   711  		{
   712  			name: "url is not https",
   713  			in:   "http://oidc.oidc-namespace.svc",
   714  			want: `issuer.discoveryURL: Invalid value: "http://oidc.oidc-namespace.svc": URL scheme must be https`,
   715  		},
   716  		{
   717  			name: "url user info is not allowed",
   718  			in:   "https://user:pass@oidc.oidc-namespace.svc",
   719  			want: `issuer.discoveryURL: Invalid value: "https://user:pass@oidc.oidc-namespace.svc": URL must not contain a username or password`,
   720  		},
   721  		{
   722  			name: "url raw query is not allowed",
   723  			in:   "https://oidc.oidc-namespace.svc?query",
   724  			want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc?query": URL must not contain a query`,
   725  		},
   726  		{
   727  			name: "url fragment is not allowed",
   728  			in:   "https://oidc.oidc-namespace.svc#fragment",
   729  			want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc#fragment": URL must not contain a fragment`,
   730  		},
   731  		{
   732  			name: "valid url",
   733  			in:   "https://oidc.oidc-namespace.svc",
   734  			want: "",
   735  		},
   736  		{
   737  			name: "valid url with path",
   738  			in:   "https://oidc.oidc-namespace.svc/path",
   739  			want: "",
   740  		},
   741  		{
   742  			name:      "discovery url same as issuer url",
   743  			issuerURL: "https://issuer-url",
   744  			in:        "https://issuer-url",
   745  			want:      `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`,
   746  		},
   747  		{
   748  			name:      "discovery url same as issuer url, with trailing slash",
   749  			issuerURL: "https://issuer-url",
   750  			in:        "https://issuer-url/",
   751  			want:      `issuer.discoveryURL: Invalid value: "https://issuer-url/": discoveryURL must be different from URL`,
   752  		},
   753  		{
   754  			name:      "discovery url same as issuer url, with multiple trailing slashes",
   755  			issuerURL: "https://issuer-url",
   756  			in:        "https://issuer-url///",
   757  			want:      `issuer.discoveryURL: Invalid value: "https://issuer-url///": discoveryURL must be different from URL`,
   758  		},
   759  		{
   760  			name:      "discovery url same as issuer url, issuer url with trailing slash",
   761  			issuerURL: "https://issuer-url/",
   762  			in:        "https://issuer-url",
   763  			want:      `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`,
   764  		},
   765  	}
   766  
   767  	for _, tt := range testCases {
   768  		t.Run(tt.name, func(t *testing.T) {
   769  			got := validateIssuerDiscoveryURL(tt.issuerURL, tt.in, fldPath).ToAggregate()
   770  			if d := cmp.Diff(tt.want, errString(got)); d != "" {
   771  				t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
   772  			}
   773  		})
   774  	}
   775  }
   776  
   777  func TestValidateAudiences(t *testing.T) {
   778  	fldPath := field.NewPath("issuer", "audiences")
   779  	audienceMatchPolicyFldPath := field.NewPath("issuer", "audienceMatchPolicy")
   780  
   781  	testCases := []struct {
   782  		name        string
   783  		in          []string
   784  		matchPolicy string
   785  		want        string
   786  	}{
   787  		{
   788  			name: "audiences is empty",
   789  			in:   []string{},
   790  			want: "issuer.audiences: Required value: at least one issuer.audiences is required",
   791  		},
   792  		{
   793  			name: "audience is empty",
   794  			in:   []string{""},
   795  			want: "issuer.audiences[0]: Required value: audience can't be empty",
   796  		},
   797  		{
   798  			name:        "invalid match policy with single audience",
   799  			in:          []string{"audience"},
   800  			matchPolicy: "MatchExact",
   801  			want:        `issuer.audienceMatchPolicy: Invalid value: "MatchExact": audienceMatchPolicy must be empty or MatchAny for single audience`,
   802  		},
   803  		{
   804  			name: "valid audience",
   805  			in:   []string{"audience"},
   806  			want: "",
   807  		},
   808  		{
   809  			name:        "valid audience with MatchAny policy",
   810  			in:          []string{"audience"},
   811  			matchPolicy: "MatchAny",
   812  			want:        "",
   813  		},
   814  		{
   815  			name:        "duplicate audience",
   816  			in:          []string{"audience", "audience"},
   817  			matchPolicy: "MatchAny",
   818  			want:        `issuer.audiences[1]: Duplicate value: "audience"`,
   819  		},
   820  		{
   821  			name: "match policy not set with multiple audiences",
   822  			in:   []string{"audience1", "audience2"},
   823  			want: `issuer.audienceMatchPolicy: Invalid value: "": audienceMatchPolicy must be MatchAny for multiple audiences`,
   824  		},
   825  		{
   826  			name:        "valid multiple audiences",
   827  			in:          []string{"audience1", "audience2"},
   828  			matchPolicy: "MatchAny",
   829  			want:        "",
   830  		},
   831  	}
   832  
   833  	for _, tt := range testCases {
   834  		t.Run(tt.name, func(t *testing.T) {
   835  			got := validateAudiences(tt.in, api.AudienceMatchPolicyType(tt.matchPolicy), fldPath, audienceMatchPolicyFldPath).ToAggregate()
   836  			if d := cmp.Diff(tt.want, errString(got)); d != "" {
   837  				t.Fatalf("Audiences validation mismatch (-want +got):\n%s", d)
   838  			}
   839  		})
   840  	}
   841  }
   842  
   843  func TestValidateCertificateAuthority(t *testing.T) {
   844  	fldPath := field.NewPath("issuer", "certificateAuthority")
   845  
   846  	testCases := []struct {
   847  		name string
   848  		in   func() string
   849  		want string
   850  	}{
   851  		{
   852  			name: "invalid certificate authority",
   853  			in:   func() string { return "invalid" },
   854  			want: `issuer.certificateAuthority: Invalid value: "<omitted>": data does not contain any valid RSA or ECDSA certificates`,
   855  		},
   856  		{
   857  			name: "certificate authority is empty",
   858  			in:   func() string { return "" },
   859  			want: "",
   860  		},
   861  		{
   862  			name: "valid certificate authority",
   863  			in: func() string {
   864  				caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   865  				if err != nil {
   866  					t.Fatal(err)
   867  				}
   868  				caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey)
   869  				if err != nil {
   870  					t.Fatal(err)
   871  				}
   872  				return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}))
   873  			},
   874  			want: "",
   875  		},
   876  	}
   877  
   878  	for _, tt := range testCases {
   879  		t.Run(tt.name, func(t *testing.T) {
   880  			got := validateCertificateAuthority(tt.in(), fldPath).ToAggregate()
   881  			if d := cmp.Diff(tt.want, errString(got)); d != "" {
   882  				t.Fatalf("CertificateAuthority validation mismatch (-want +got):\n%s", d)
   883  			}
   884  		})
   885  	}
   886  }
   887  
   888  func TestValidateClaimValidationRules(t *testing.T) {
   889  	fldPath := field.NewPath("issuer", "claimValidationRules")
   890  
   891  	testCases := []struct {
   892  		name                          string
   893  		in                            []api.ClaimValidationRule
   894  		structuredAuthnFeatureEnabled bool
   895  		want                          string
   896  		wantCELMapper                 bool
   897  		wantUsesEmailVerifiedClaim    bool
   898  	}{
   899  		{
   900  			name:                          "claim and expression are empty, structured authn feature enabled",
   901  			in:                            []api.ClaimValidationRule{{}},
   902  			structuredAuthnFeatureEnabled: true,
   903  			want:                          "issuer.claimValidationRules[0]: Required value: claim or expression is required",
   904  		},
   905  		{
   906  			name: "claim and expression are set",
   907  			in: []api.ClaimValidationRule{
   908  				{Claim: "claim", Expression: "expression"},
   909  			},
   910  			structuredAuthnFeatureEnabled: true,
   911  			want:                          `issuer.claimValidationRules[0]: Invalid value: "claim": claim and expression can't both be set`,
   912  		},
   913  		{
   914  			name: "message set when claim is set",
   915  			in: []api.ClaimValidationRule{
   916  				{Claim: "claim", Message: "message"},
   917  			},
   918  			structuredAuthnFeatureEnabled: true,
   919  			want:                          `issuer.claimValidationRules[0].message: Invalid value: "message": message can't be set when claim is set`,
   920  		},
   921  		{
   922  			name: "requiredValue set when expression is set",
   923  			in: []api.ClaimValidationRule{
   924  				{Expression: "claims.foo == 'bar'", RequiredValue: "value"},
   925  			},
   926  			structuredAuthnFeatureEnabled: true,
   927  			want:                          `issuer.claimValidationRules[0].requiredValue: Invalid value: "value": requiredValue can't be set when expression is set`,
   928  		},
   929  		{
   930  			name: "duplicate claim",
   931  			in: []api.ClaimValidationRule{
   932  				{Claim: "claim"},
   933  				{Claim: "claim"},
   934  			},
   935  			structuredAuthnFeatureEnabled: true,
   936  			want:                          `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`,
   937  		},
   938  		{
   939  			name: "duplicate expression",
   940  			in: []api.ClaimValidationRule{
   941  				{Expression: "claims.foo == 'bar'"},
   942  				{Expression: "claims.foo == 'bar'"},
   943  			},
   944  			structuredAuthnFeatureEnabled: true,
   945  			want:                          `issuer.claimValidationRules[1].expression: Duplicate value: "claims.foo == 'bar'"`,
   946  		},
   947  		{
   948  			name: "expression set when structured authn feature is disabled",
   949  			in: []api.ClaimValidationRule{
   950  				{Expression: "claims.foo == 'bar'"},
   951  			},
   952  			structuredAuthnFeatureEnabled: false,
   953  			want:                          `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo == 'bar'": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
   954  		},
   955  		{
   956  			name: "CEL expression compilation error",
   957  			in: []api.ClaimValidationRule{
   958  				{Expression: "foo.bar"},
   959  			},
   960  			structuredAuthnFeatureEnabled: true,
   961  			want: `issuer.claimValidationRules[0].expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
   962   | foo.bar
   963   | ^`,
   964  		},
   965  		{
   966  			name: "expression does not evaluate to bool",
   967  			in: []api.ClaimValidationRule{
   968  				{Expression: "claims.foo"},
   969  			},
   970  			structuredAuthnFeatureEnabled: true,
   971  			want:                          `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo": must evaluate to bool`,
   972  		},
   973  		{
   974  			name: "valid claim validation rule with expression",
   975  			in: []api.ClaimValidationRule{
   976  				{Expression: "claims.foo == 'bar'"},
   977  			},
   978  			structuredAuthnFeatureEnabled: true,
   979  			want:                          "",
   980  			wantCELMapper:                 true,
   981  		},
   982  		{
   983  			name: "valid claim validation rule with multiple rules and email_verified check",
   984  			in: []api.ClaimValidationRule{
   985  				{Claim: "claim1", RequiredValue: "value1"},
   986  				{Claim: "claim2", RequiredValue: "value2"},
   987  				{Expression: "has(claims.email_verified)"},
   988  			},
   989  			structuredAuthnFeatureEnabled: true,
   990  			want:                          "",
   991  			wantUsesEmailVerifiedClaim:    true,
   992  		},
   993  		{
   994  			name: "valid claim validation rule with multiple rules and almost email_verified check",
   995  			in: []api.ClaimValidationRule{
   996  				{Claim: "claim1", RequiredValue: "value1"},
   997  				{Claim: "claim2", RequiredValue: "value2"},
   998  				{Expression: "has(claims.email_verified_)"},
   999  			},
  1000  			structuredAuthnFeatureEnabled: true,
  1001  			want:                          "",
  1002  			wantUsesEmailVerifiedClaim:    false,
  1003  		},
  1004  		{
  1005  			name: "valid claim validation rule with multiple rules",
  1006  			in: []api.ClaimValidationRule{
  1007  				{Claim: "claim1", RequiredValue: "value1"},
  1008  				{Claim: "claim2", RequiredValue: "claims.email_verified"}, // not a CEL expression
  1009  			},
  1010  			structuredAuthnFeatureEnabled: true,
  1011  			want:                          "",
  1012  			wantUsesEmailVerifiedClaim:    false,
  1013  		},
  1014  	}
  1015  
  1016  	for _, tt := range testCases {
  1017  		t.Run(tt.name, func(t *testing.T) {
  1018  			state := &validationState{}
  1019  			got := validateClaimValidationRules(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
  1020  			if d := cmp.Diff(tt.want, errString(got)); d != "" {
  1021  				t.Fatalf("ClaimValidationRules validation mismatch (-want +got):\n%s", d)
  1022  			}
  1023  			if tt.wantCELMapper && state.mapper.ClaimValidationRules == nil {
  1024  				t.Fatalf("ClaimValidationRules validation mismatch: CELMapper.ClaimValidationRules is nil")
  1025  			}
  1026  			if tt.wantUsesEmailVerifiedClaim != state.usesEmailVerifiedClaim {
  1027  				t.Fatalf("ClaimValidationRules state.usesEmailVerifiedClaim mismatch: want %v, got %v", tt.wantUsesEmailVerifiedClaim, state.usesEmailVerifiedClaim)
  1028  			}
  1029  		})
  1030  	}
  1031  }
  1032  
  1033  func TestValidateClaimMappings(t *testing.T) {
  1034  	fldPath := field.NewPath("issuer", "claimMappings")
  1035  
  1036  	testCases := []struct {
  1037  		name                          string
  1038  		in                            api.ClaimMappings
  1039  		usesEmailVerifiedClaim        bool
  1040  		structuredAuthnFeatureEnabled bool
  1041  		want                          string
  1042  		wantCELMapper                 bool
  1043  	}{
  1044  		{
  1045  			name: "username expression and claim are set",
  1046  			in: api.ClaimMappings{
  1047  				Username: api.PrefixedClaimOrExpression{
  1048  					Claim:      "claim",
  1049  					Expression: "claims.username",
  1050  				},
  1051  			},
  1052  			structuredAuthnFeatureEnabled: true,
  1053  			want:                          `issuer.claimMappings.username: Invalid value: "": claim and expression can't both be set`,
  1054  		},
  1055  		{
  1056  			name:                          "username expression and claim are empty",
  1057  			in:                            api.ClaimMappings{Username: api.PrefixedClaimOrExpression{}},
  1058  			structuredAuthnFeatureEnabled: true,
  1059  			want:                          "issuer.claimMappings.username: Required value: claim or expression is required",
  1060  		},
  1061  		{
  1062  			name: "username prefix set when expression is set",
  1063  			in: api.ClaimMappings{
  1064  				Username: api.PrefixedClaimOrExpression{
  1065  					Expression: "claims.username",
  1066  					Prefix:     pointer.String("prefix"),
  1067  				},
  1068  			},
  1069  			structuredAuthnFeatureEnabled: true,
  1070  			want:                          `issuer.claimMappings.username.prefix: Invalid value: "prefix": prefix can't be set when expression is set`,
  1071  		},
  1072  		{
  1073  			name: "username prefix is nil when claim is set",
  1074  			in: api.ClaimMappings{
  1075  				Username: api.PrefixedClaimOrExpression{
  1076  					Claim: "claim",
  1077  				},
  1078  			},
  1079  			structuredAuthnFeatureEnabled: true,
  1080  			want:                          `issuer.claimMappings.username.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`,
  1081  		},
  1082  		{
  1083  			name: "username expression is invalid",
  1084  			in: api.ClaimMappings{
  1085  				Username: api.PrefixedClaimOrExpression{
  1086  					Expression: "foo.bar",
  1087  				},
  1088  			},
  1089  			structuredAuthnFeatureEnabled: true,
  1090  			want: `issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
  1091   | foo.bar
  1092   | ^`,
  1093  		},
  1094  		{
  1095  			name: "groups expression and claim are set",
  1096  			in: api.ClaimMappings{
  1097  				Username: api.PrefixedClaimOrExpression{
  1098  					Claim:  "claim",
  1099  					Prefix: pointer.String("prefix"),
  1100  				},
  1101  				Groups: api.PrefixedClaimOrExpression{
  1102  					Claim:      "claim",
  1103  					Expression: "claims.groups",
  1104  				},
  1105  			},
  1106  			structuredAuthnFeatureEnabled: true,
  1107  			want:                          `issuer.claimMappings.groups: Invalid value: "": claim and expression can't both be set`,
  1108  		},
  1109  		{
  1110  			name: "groups prefix set when expression is set",
  1111  			in: api.ClaimMappings{
  1112  				Username: api.PrefixedClaimOrExpression{
  1113  					Claim:  "claim",
  1114  					Prefix: pointer.String("prefix"),
  1115  				},
  1116  				Groups: api.PrefixedClaimOrExpression{
  1117  					Expression: "claims.groups",
  1118  					Prefix:     pointer.String("prefix"),
  1119  				},
  1120  			},
  1121  			structuredAuthnFeatureEnabled: true,
  1122  			want:                          `issuer.claimMappings.groups.prefix: Invalid value: "prefix": prefix can't be set when expression is set`,
  1123  		},
  1124  		{
  1125  			name: "groups prefix is nil when claim is set",
  1126  			in: api.ClaimMappings{
  1127  				Username: api.PrefixedClaimOrExpression{
  1128  					Claim:  "claim",
  1129  					Prefix: pointer.String("prefix"),
  1130  				},
  1131  				Groups: api.PrefixedClaimOrExpression{
  1132  					Claim: "claim",
  1133  				},
  1134  			},
  1135  			structuredAuthnFeatureEnabled: true,
  1136  			want:                          `issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`,
  1137  		},
  1138  		{
  1139  			name: "groups expression is invalid",
  1140  			in: api.ClaimMappings{
  1141  				Username: api.PrefixedClaimOrExpression{
  1142  					Claim:  "claim",
  1143  					Prefix: pointer.String("prefix"),
  1144  				},
  1145  				Groups: api.PrefixedClaimOrExpression{
  1146  					Expression: "foo.bar",
  1147  				},
  1148  			},
  1149  			structuredAuthnFeatureEnabled: true,
  1150  			want: `issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
  1151   | foo.bar
  1152   | ^`,
  1153  		},
  1154  		{
  1155  			name: "uid claim and expression are set",
  1156  			in: api.ClaimMappings{
  1157  				Username: api.PrefixedClaimOrExpression{
  1158  					Claim:  "claim",
  1159  					Prefix: pointer.String("prefix"),
  1160  				},
  1161  				UID: api.ClaimOrExpression{
  1162  					Claim:      "claim",
  1163  					Expression: "claims.uid",
  1164  				},
  1165  			},
  1166  			structuredAuthnFeatureEnabled: true,
  1167  			want:                          `issuer.claimMappings.uid: Invalid value: "": claim and expression can't both be set`,
  1168  		},
  1169  		{
  1170  			name: "uid expression is invalid",
  1171  			in: api.ClaimMappings{
  1172  				Username: api.PrefixedClaimOrExpression{
  1173  					Claim:  "claim",
  1174  					Prefix: pointer.String("prefix"),
  1175  				},
  1176  				UID: api.ClaimOrExpression{
  1177  					Expression: "foo.bar",
  1178  				},
  1179  			},
  1180  			structuredAuthnFeatureEnabled: true,
  1181  			want: `issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
  1182   | foo.bar
  1183   | ^`,
  1184  		},
  1185  		{
  1186  			name: "extra mapping key is empty",
  1187  			in: api.ClaimMappings{
  1188  				Username: api.PrefixedClaimOrExpression{
  1189  					Claim:  "claim",
  1190  					Prefix: pointer.String("prefix"),
  1191  				},
  1192  				Extra: []api.ExtraMapping{
  1193  					{Key: "", ValueExpression: "claims.extra"},
  1194  				},
  1195  			},
  1196  			structuredAuthnFeatureEnabled: true,
  1197  			want:                          `issuer.claimMappings.extra[0].key: Required value`,
  1198  		},
  1199  		{
  1200  			name: "extra mapping value expression is empty",
  1201  			in: api.ClaimMappings{
  1202  				Username: api.PrefixedClaimOrExpression{
  1203  					Claim:  "claim",
  1204  					Prefix: pointer.String("prefix"),
  1205  				},
  1206  				Extra: []api.ExtraMapping{
  1207  					{Key: "example.org/foo", ValueExpression: ""},
  1208  				},
  1209  			},
  1210  			structuredAuthnFeatureEnabled: true,
  1211  			want:                          `issuer.claimMappings.extra[0].valueExpression: Required value: valueExpression is required`,
  1212  		},
  1213  		{
  1214  			name: "extra mapping value expression is invalid",
  1215  			in: api.ClaimMappings{
  1216  				Username: api.PrefixedClaimOrExpression{
  1217  					Claim:  "claim",
  1218  					Prefix: pointer.String("prefix"),
  1219  				},
  1220  				Extra: []api.ExtraMapping{
  1221  					{Key: "example.org/foo", ValueExpression: "foo.bar"},
  1222  				},
  1223  			},
  1224  			structuredAuthnFeatureEnabled: true,
  1225  			want: `issuer.claimMappings.extra[0].valueExpression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
  1226   | foo.bar
  1227   | ^`,
  1228  		},
  1229  		{
  1230  			name: "username expression is invalid when structured authn feature is disabled",
  1231  			in: api.ClaimMappings{
  1232  				Username: api.PrefixedClaimOrExpression{
  1233  					Expression: "foo.bar",
  1234  				},
  1235  			},
  1236  			structuredAuthnFeatureEnabled: false,
  1237  			want: `[issuer.claimMappings.username.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
  1238   | foo.bar
  1239   | ^]`,
  1240  		},
  1241  		{
  1242  			name: "groups expression is invalid when structured authn feature is disabled",
  1243  			in: api.ClaimMappings{
  1244  				Username: api.PrefixedClaimOrExpression{
  1245  					Claim:  "claim",
  1246  					Prefix: pointer.String("prefix"),
  1247  				},
  1248  				Groups: api.PrefixedClaimOrExpression{
  1249  					Expression: "foo.bar",
  1250  				},
  1251  			},
  1252  			structuredAuthnFeatureEnabled: false,
  1253  			want: `[issuer.claimMappings.groups.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
  1254   | foo.bar
  1255   | ^]`,
  1256  		},
  1257  		{
  1258  			name: "uid expression is invalid when structured authn feature is disabled",
  1259  			in: api.ClaimMappings{
  1260  				Username: api.PrefixedClaimOrExpression{
  1261  					Claim:  "claim",
  1262  					Prefix: pointer.String("prefix"),
  1263  				},
  1264  				UID: api.ClaimOrExpression{
  1265  					Expression: "foo.bar",
  1266  				},
  1267  			},
  1268  			structuredAuthnFeatureEnabled: false,
  1269  			want: `[issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
  1270   | foo.bar
  1271   | ^]`,
  1272  		},
  1273  		{
  1274  			name: "uid claim is invalid when structured authn feature is disabled",
  1275  			in: api.ClaimMappings{
  1276  				Username: api.PrefixedClaimOrExpression{
  1277  					Claim:  "claim",
  1278  					Prefix: pointer.String("prefix"),
  1279  				},
  1280  				UID: api.ClaimOrExpression{
  1281  					Claim: "claim",
  1282  				},
  1283  			},
  1284  			structuredAuthnFeatureEnabled: false,
  1285  			want:                          `issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
  1286  		},
  1287  		{
  1288  			name: "extra mapping is invalid when structured authn feature is disabled",
  1289  			in: api.ClaimMappings{
  1290  				Username: api.PrefixedClaimOrExpression{
  1291  					Claim:  "claim",
  1292  					Prefix: pointer.String("prefix"),
  1293  				},
  1294  				Extra: []api.ExtraMapping{
  1295  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1296  				},
  1297  			},
  1298  			structuredAuthnFeatureEnabled: false,
  1299  			want:                          `issuer.claimMappings.extra: Invalid value: "": extra claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
  1300  		},
  1301  		{
  1302  			name: "duplicate extra mapping key",
  1303  			in: api.ClaimMappings{
  1304  				Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
  1305  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1306  				Extra: []api.ExtraMapping{
  1307  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1308  					{Key: "example.org/foo", ValueExpression: "claims.extras"},
  1309  				},
  1310  			},
  1311  			structuredAuthnFeatureEnabled: true,
  1312  			want:                          `issuer.claimMappings.extra[1].key: Duplicate value: "example.org/foo"`,
  1313  		},
  1314  		{
  1315  			name: "extra mapping key is not domain prefix path",
  1316  			in: api.ClaimMappings{
  1317  				Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
  1318  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1319  				Extra: []api.ExtraMapping{
  1320  					{Key: "foo", ValueExpression: "claims.extra"},
  1321  				},
  1322  			},
  1323  			structuredAuthnFeatureEnabled: true,
  1324  			want:                          `issuer.claimMappings.extra[0].key: Invalid value: "foo": must be a domain-prefixed path (such as "acme.io/foo")`,
  1325  		},
  1326  		{
  1327  			name: "extra mapping key is not lower case",
  1328  			in: api.ClaimMappings{
  1329  				Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
  1330  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1331  				Extra: []api.ExtraMapping{
  1332  					{Key: "example.org/Foo", ValueExpression: "claims.extra"},
  1333  				},
  1334  			},
  1335  			structuredAuthnFeatureEnabled: true,
  1336  			want:                          `issuer.claimMappings.extra[0].key: Invalid value: "example.org/Foo": key must be lowercase`,
  1337  		},
  1338  		{
  1339  			name: "valid claim mappings but uses email without verification",
  1340  			in: api.ClaimMappings{
  1341  				Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
  1342  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1343  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1344  				Extra: []api.ExtraMapping{
  1345  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1346  				},
  1347  			},
  1348  			structuredAuthnFeatureEnabled: true,
  1349  			wantCELMapper:                 true,
  1350  			want:                          `issuer.claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
  1351  		},
  1352  		{
  1353  			name: "valid claim mappings but uses email in complex CEL expression without verification",
  1354  			in: api.ClaimMappings{
  1355  				Username: api.PrefixedClaimOrExpression{Expression: "has(claims.email) ? claims.email : claims.sub"},
  1356  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1357  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1358  				Extra: []api.ExtraMapping{
  1359  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1360  				},
  1361  			},
  1362  			structuredAuthnFeatureEnabled: true,
  1363  			wantCELMapper:                 true,
  1364  			want:                          `issuer.claimMappings.username.expression: Invalid value: "has(claims.email) ? claims.email : claims.sub": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
  1365  		},
  1366  		{
  1367  			name: "valid claim mappings but uses email in CEL expression function without verification",
  1368  			in: api.ClaimMappings{
  1369  				Username: api.PrefixedClaimOrExpression{Expression: "claims.email.trim()"},
  1370  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1371  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1372  				Extra: []api.ExtraMapping{
  1373  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1374  				},
  1375  			},
  1376  			structuredAuthnFeatureEnabled: true,
  1377  			wantCELMapper:                 true,
  1378  			want:                          `issuer.claimMappings.username.expression: Invalid value: "claims.email.trim()": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
  1379  		},
  1380  		{
  1381  			name: "valid claim mappings and uses email with verification via extra",
  1382  			in: api.ClaimMappings{
  1383  				Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
  1384  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1385  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1386  				Extra: []api.ExtraMapping{
  1387  					{Key: "example.org/foo", ValueExpression: "claims.email_verified"},
  1388  				},
  1389  			},
  1390  			structuredAuthnFeatureEnabled: true,
  1391  			wantCELMapper:                 true,
  1392  			want:                          "",
  1393  		},
  1394  		{
  1395  			name: "valid claim mappings and uses email with verification via extra optional",
  1396  			in: api.ClaimMappings{
  1397  				Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
  1398  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1399  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1400  				Extra: []api.ExtraMapping{
  1401  					{Key: "example.org/foo", ValueExpression: `has(claims.email_verified) ? string(claims.email_verified) : "false"`},
  1402  				},
  1403  			},
  1404  			structuredAuthnFeatureEnabled: true,
  1405  			wantCELMapper:                 true,
  1406  			want:                          "",
  1407  		},
  1408  		{
  1409  			name: "valid claim mappings and almost uses email with verification via extra optional",
  1410  			in: api.ClaimMappings{
  1411  				Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
  1412  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1413  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1414  				Extra: []api.ExtraMapping{
  1415  					{Key: "example.org/foo", ValueExpression: `has(claims.email_verified_) ? string(claims.email_verified_) : "false"`},
  1416  				},
  1417  			},
  1418  			structuredAuthnFeatureEnabled: true,
  1419  			wantCELMapper:                 true,
  1420  			want:                          `issuer.claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
  1421  		},
  1422  		{
  1423  			name: "valid claim mappings and uses email with verification via hasVerifiedEmail",
  1424  			in: api.ClaimMappings{
  1425  				Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
  1426  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1427  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1428  				Extra: []api.ExtraMapping{
  1429  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1430  				},
  1431  			},
  1432  			usesEmailVerifiedClaim:        true,
  1433  			structuredAuthnFeatureEnabled: true,
  1434  			wantCELMapper:                 true,
  1435  			want:                          "",
  1436  		},
  1437  		{
  1438  			name: "valid claim mappings that almost use claims.email",
  1439  			in: api.ClaimMappings{
  1440  				Username: api.PrefixedClaimOrExpression{Expression: "claims.email_"},
  1441  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1442  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1443  				Extra: []api.ExtraMapping{
  1444  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1445  				},
  1446  			},
  1447  			structuredAuthnFeatureEnabled: true,
  1448  			wantCELMapper:                 true,
  1449  			want:                          "",
  1450  		},
  1451  		{
  1452  			name: "valid claim mappings that almost use claims.email via nesting",
  1453  			in: api.ClaimMappings{
  1454  				Username: api.PrefixedClaimOrExpression{Expression: "claims.other.claims.email"},
  1455  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1456  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1457  				Extra: []api.ExtraMapping{
  1458  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1459  				},
  1460  			},
  1461  			structuredAuthnFeatureEnabled: true,
  1462  			wantCELMapper:                 true,
  1463  			want:                          "",
  1464  		},
  1465  		{
  1466  			name: "valid claim mappings",
  1467  			in: api.ClaimMappings{
  1468  				Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
  1469  				Groups:   api.PrefixedClaimOrExpression{Expression: "claims.groups"},
  1470  				UID:      api.ClaimOrExpression{Expression: "claims.uid"},
  1471  				Extra: []api.ExtraMapping{
  1472  					{Key: "example.org/foo", ValueExpression: "claims.extra"},
  1473  				},
  1474  			},
  1475  			structuredAuthnFeatureEnabled: true,
  1476  			wantCELMapper:                 true,
  1477  			want:                          "",
  1478  		},
  1479  	}
  1480  
  1481  	for _, tt := range testCases {
  1482  		t.Run(tt.name, func(t *testing.T) {
  1483  			state := &validationState{usesEmailVerifiedClaim: tt.usesEmailVerifiedClaim}
  1484  			got := validateClaimMappings(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
  1485  			if d := cmp.Diff(tt.want, errString(got)); d != "" {
  1486  				fmt.Println(errString(got))
  1487  				t.Fatalf("ClaimMappings validation mismatch (-want +got):\n%s", d)
  1488  			}
  1489  			if tt.wantCELMapper {
  1490  				if len(tt.in.Username.Expression) > 0 && state.mapper.Username == nil {
  1491  					t.Fatalf("ClaimMappings validation mismatch: CELMapper.Username is nil")
  1492  				}
  1493  				if len(tt.in.Groups.Expression) > 0 && state.mapper.Groups == nil {
  1494  					t.Fatalf("ClaimMappings validation mismatch: CELMapper.Groups is nil")
  1495  				}
  1496  				if len(tt.in.UID.Expression) > 0 && state.mapper.UID == nil {
  1497  					t.Fatalf("ClaimMappings validation mismatch: CELMapper.UID is nil")
  1498  				}
  1499  				if len(tt.in.Extra) > 0 && state.mapper.Extra == nil {
  1500  					t.Fatalf("ClaimMappings validation mismatch: CELMapper.Extra is nil")
  1501  				}
  1502  			}
  1503  		})
  1504  	}
  1505  }
  1506  
  1507  func TestValidateUserValidationRules(t *testing.T) {
  1508  	fldPath := field.NewPath("issuer", "userValidationRules")
  1509  
  1510  	testCases := []struct {
  1511  		name                          string
  1512  		in                            []api.UserValidationRule
  1513  		structuredAuthnFeatureEnabled bool
  1514  		want                          string
  1515  		wantCELMapper                 bool
  1516  	}{
  1517  		{
  1518  			name:                          "user info validation rule, expression is empty",
  1519  			in:                            []api.UserValidationRule{{}},
  1520  			structuredAuthnFeatureEnabled: true,
  1521  			want:                          "issuer.userValidationRules[0].expression: Required value: expression is required",
  1522  		},
  1523  		{
  1524  			name: "duplicate expression",
  1525  			in: []api.UserValidationRule{
  1526  				{Expression: "user.username == 'foo'"},
  1527  				{Expression: "user.username == 'foo'"},
  1528  			},
  1529  			structuredAuthnFeatureEnabled: true,
  1530  			want:                          `issuer.userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`,
  1531  		},
  1532  		{
  1533  			name: "user validation rule is invalid when structured authn feature is disabled",
  1534  			in: []api.UserValidationRule{
  1535  				{Expression: "user.username == 'foo'"},
  1536  			},
  1537  			structuredAuthnFeatureEnabled: false,
  1538  			want:                          `issuer.userValidationRules: Invalid value: "": user validation rules are not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
  1539  		},
  1540  		{
  1541  			name: "expression is invalid",
  1542  			in: []api.UserValidationRule{
  1543  				{Expression: "foo.bar"},
  1544  			},
  1545  			structuredAuthnFeatureEnabled: true,
  1546  			want: `issuer.userValidationRules[0].expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
  1547   | foo.bar
  1548   | ^`,
  1549  		},
  1550  		{
  1551  			name: "expression does not return bool",
  1552  			in: []api.UserValidationRule{
  1553  				{Expression: "user.username"},
  1554  			},
  1555  			structuredAuthnFeatureEnabled: true,
  1556  			want:                          `issuer.userValidationRules[0].expression: Invalid value: "user.username": must evaluate to bool`,
  1557  		},
  1558  		{
  1559  			name: "valid user info validation rule",
  1560  			in: []api.UserValidationRule{
  1561  				{Expression: "user.username == 'foo'"},
  1562  				{Expression: "!user.username.startsWith('system:')", Message: "username cannot used reserved system: prefix"},
  1563  			},
  1564  			structuredAuthnFeatureEnabled: true,
  1565  			want:                          "",
  1566  			wantCELMapper:                 true,
  1567  		},
  1568  	}
  1569  
  1570  	for _, tt := range testCases {
  1571  		t.Run(tt.name, func(t *testing.T) {
  1572  			state := &validationState{}
  1573  			got := validateUserValidationRules(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
  1574  			if d := cmp.Diff(tt.want, errString(got)); d != "" {
  1575  				t.Fatalf("UserValidationRules validation mismatch (-want +got):\n%s", d)
  1576  			}
  1577  			if tt.wantCELMapper && state.mapper.UserValidationRules == nil {
  1578  				t.Fatalf("UserValidationRules validation mismatch: CELMapper.UserValidationRules is nil")
  1579  			}
  1580  		})
  1581  	}
  1582  }
  1583  
  1584  func errString(errs errors.Aggregate) string {
  1585  	if errs != nil {
  1586  		return errs.Error()
  1587  	}
  1588  	return ""
  1589  }
  1590  
  1591  type (
  1592  	test struct {
  1593  		name            string
  1594  		configuration   api.AuthorizationConfiguration
  1595  		expectedErrList field.ErrorList
  1596  		knownTypes      sets.String
  1597  		repeatableTypes sets.String
  1598  	}
  1599  )
  1600  
  1601  func TestValidateAuthorizationConfiguration(t *testing.T) {
  1602  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)
  1603  
  1604  	badKubeConfigFile := "../some/relative/path/kubeconfig"
  1605  
  1606  	tempKubeConfigFile, err := os.CreateTemp("/tmp", "kubeconfig")
  1607  	if err != nil {
  1608  		t.Fatalf("failed to set up temp file: %v", err)
  1609  	}
  1610  	tempKubeConfigFilePath := tempKubeConfigFile.Name()
  1611  	defer os.Remove(tempKubeConfigFilePath)
  1612  
  1613  	tests := []test{
  1614  		{
  1615  			name: "atleast one authorizer should be defined",
  1616  			configuration: api.AuthorizationConfiguration{
  1617  				Authorizers: []api.AuthorizerConfiguration{},
  1618  			},
  1619  			expectedErrList: field.ErrorList{field.Required(field.NewPath("authorizers"), "at least one authorization mode must be defined")},
  1620  			knownTypes:      sets.NewString(),
  1621  			repeatableTypes: sets.NewString(),
  1622  		},
  1623  		{
  1624  			name: "type and name are required if an authorizer is defined",
  1625  			configuration: api.AuthorizationConfiguration{
  1626  				Authorizers: []api.AuthorizerConfiguration{
  1627  					{},
  1628  				},
  1629  			},
  1630  			expectedErrList: field.ErrorList{field.Required(field.NewPath("type"), "")},
  1631  			knownTypes:      sets.NewString(string("Webhook")),
  1632  			repeatableTypes: sets.NewString(string("Webhook")),
  1633  		},
  1634  		{
  1635  			name: "authorizer names should be of non-zero length",
  1636  			configuration: api.AuthorizationConfiguration{
  1637  				Authorizers: []api.AuthorizerConfiguration{
  1638  					{
  1639  						Type: "Foo",
  1640  						Name: "",
  1641  					},
  1642  				},
  1643  			},
  1644  			expectedErrList: field.ErrorList{field.Required(field.NewPath("name"), "")},
  1645  			knownTypes:      sets.NewString(string("Foo")),
  1646  			repeatableTypes: sets.NewString(string("Webhook")),
  1647  		},
  1648  		{
  1649  			name: "authorizer names should be unique",
  1650  			configuration: api.AuthorizationConfiguration{
  1651  				Authorizers: []api.AuthorizerConfiguration{
  1652  					{
  1653  						Type: "Foo",
  1654  						Name: "foo",
  1655  					},
  1656  					{
  1657  						Type: "Bar",
  1658  						Name: "foo",
  1659  					},
  1660  				},
  1661  			},
  1662  			expectedErrList: field.ErrorList{field.Duplicate(field.NewPath("name"), "foo")},
  1663  			knownTypes:      sets.NewString(string("Foo"), string("Bar")),
  1664  			repeatableTypes: sets.NewString(string("Webhook")),
  1665  		},
  1666  		{
  1667  			name: "authorizer names should be DNS1123 labels",
  1668  			configuration: api.AuthorizationConfiguration{
  1669  				Authorizers: []api.AuthorizerConfiguration{
  1670  					{
  1671  						Type: "Foo",
  1672  						Name: "myauthorizer",
  1673  					},
  1674  				},
  1675  			},
  1676  			expectedErrList: field.ErrorList{},
  1677  			knownTypes:      sets.NewString(string("Foo")),
  1678  			repeatableTypes: sets.NewString(string("Webhook")),
  1679  		},
  1680  		{
  1681  			name: "authorizer names should be DNS1123 subdomains",
  1682  			configuration: api.AuthorizationConfiguration{
  1683  				Authorizers: []api.AuthorizerConfiguration{
  1684  					{
  1685  						Type: "Foo",
  1686  						Name: "foo.example.domain",
  1687  					},
  1688  				},
  1689  			},
  1690  			expectedErrList: field.ErrorList{},
  1691  			knownTypes:      sets.NewString(string("Foo")),
  1692  			repeatableTypes: sets.NewString(string("Webhook")),
  1693  		},
  1694  		{
  1695  			name: "authorizer names should not be invalid DNS1123 labels or subdomains",
  1696  			configuration: api.AuthorizationConfiguration{
  1697  				Authorizers: []api.AuthorizerConfiguration{
  1698  					{
  1699  						Type: "Foo",
  1700  						Name: "FOO.example.domain",
  1701  					},
  1702  				},
  1703  			},
  1704  			expectedErrList: field.ErrorList{field.Invalid(field.NewPath("name"), "FOO.example.domain", "")},
  1705  			knownTypes:      sets.NewString(string("Foo")),
  1706  			repeatableTypes: sets.NewString(string("Webhook")),
  1707  		},
  1708  		{
  1709  			name: "bare minimum configuration with Webhook",
  1710  			configuration: api.AuthorizationConfiguration{
  1711  				Authorizers: []api.AuthorizerConfiguration{
  1712  					{
  1713  						Type: "Webhook",
  1714  						Name: "default",
  1715  						Webhook: &api.WebhookConfiguration{
  1716  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  1717  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  1718  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1719  							FailurePolicy:                            "NoOpinion",
  1720  							SubjectAccessReviewVersion:               "v1",
  1721  							MatchConditionSubjectAccessReviewVersion: "v1",
  1722  							ConnectionInfo: api.WebhookConnectionInfo{
  1723  								Type: "InClusterConfig",
  1724  							},
  1725  						},
  1726  					},
  1727  				},
  1728  			},
  1729  			expectedErrList: field.ErrorList{},
  1730  			knownTypes:      sets.NewString(string("Webhook")),
  1731  			repeatableTypes: sets.NewString(string("Webhook")),
  1732  		},
  1733  		{
  1734  			name: "bare minimum configuration with Webhook and MatchConditions",
  1735  			configuration: api.AuthorizationConfiguration{
  1736  				Authorizers: []api.AuthorizerConfiguration{
  1737  					{
  1738  						Type: "Webhook",
  1739  						Name: "default",
  1740  						Webhook: &api.WebhookConfiguration{
  1741  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  1742  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  1743  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1744  							FailurePolicy:                            "NoOpinion",
  1745  							SubjectAccessReviewVersion:               "v1",
  1746  							MatchConditionSubjectAccessReviewVersion: "v1",
  1747  							ConnectionInfo: api.WebhookConnectionInfo{
  1748  								Type: "InClusterConfig",
  1749  							},
  1750  							MatchConditions: []api.WebhookMatchCondition{
  1751  								{
  1752  									Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
  1753  								},
  1754  								{
  1755  									Expression: "request.user == 'admin'",
  1756  								},
  1757  							},
  1758  						},
  1759  					},
  1760  				},
  1761  			},
  1762  			expectedErrList: field.ErrorList{},
  1763  			knownTypes:      sets.NewString(string("Webhook")),
  1764  			repeatableTypes: sets.NewString(string("Webhook")),
  1765  		},
  1766  		{
  1767  			name: "bare minimum configuration with multiple webhooks",
  1768  			configuration: api.AuthorizationConfiguration{
  1769  				Authorizers: []api.AuthorizerConfiguration{
  1770  					{
  1771  						Type: "Webhook",
  1772  						Name: "default",
  1773  						Webhook: &api.WebhookConfiguration{
  1774  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  1775  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  1776  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1777  							FailurePolicy:                            "NoOpinion",
  1778  							SubjectAccessReviewVersion:               "v1",
  1779  							MatchConditionSubjectAccessReviewVersion: "v1",
  1780  							ConnectionInfo: api.WebhookConnectionInfo{
  1781  								Type: "InClusterConfig",
  1782  							},
  1783  						},
  1784  					},
  1785  					{
  1786  						Type: "Webhook",
  1787  						Name: "second-webhook",
  1788  						Webhook: &api.WebhookConfiguration{
  1789  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  1790  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  1791  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1792  							FailurePolicy:                            "NoOpinion",
  1793  							SubjectAccessReviewVersion:               "v1",
  1794  							MatchConditionSubjectAccessReviewVersion: "v1",
  1795  							ConnectionInfo: api.WebhookConnectionInfo{
  1796  								Type: "InClusterConfig",
  1797  							},
  1798  						},
  1799  					},
  1800  				},
  1801  			},
  1802  			expectedErrList: field.ErrorList{},
  1803  			knownTypes:      sets.NewString(string("Webhook")),
  1804  			repeatableTypes: sets.NewString(string("Webhook")),
  1805  		},
  1806  		{
  1807  			name: "configuration with unknown types",
  1808  			configuration: api.AuthorizationConfiguration{
  1809  				Authorizers: []api.AuthorizerConfiguration{
  1810  					{
  1811  						Type: "Foo",
  1812  					},
  1813  				},
  1814  			},
  1815  			expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("type"), "Foo", []string{"..."})},
  1816  			knownTypes:      sets.NewString(string("Webhook")),
  1817  			repeatableTypes: sets.NewString(string("Webhook")),
  1818  		},
  1819  		{
  1820  			name: "configuration with not repeatable types",
  1821  			configuration: api.AuthorizationConfiguration{
  1822  				Authorizers: []api.AuthorizerConfiguration{
  1823  					{
  1824  						Type: "Foo",
  1825  						Name: "foo-1",
  1826  					},
  1827  					{
  1828  						Type: "Foo",
  1829  						Name: "foo-2",
  1830  					},
  1831  				},
  1832  			},
  1833  			expectedErrList: field.ErrorList{field.Duplicate(field.NewPath("type"), "Foo")},
  1834  			knownTypes:      sets.NewString(string("Foo")),
  1835  			repeatableTypes: sets.NewString(string("Webhook")),
  1836  		},
  1837  		{
  1838  			name: "when type=Webhook, webhook needs to be defined",
  1839  			configuration: api.AuthorizationConfiguration{
  1840  				Authorizers: []api.AuthorizerConfiguration{
  1841  					{
  1842  						Type: "Webhook",
  1843  						Name: "default",
  1844  					},
  1845  				},
  1846  			},
  1847  			expectedErrList: field.ErrorList{field.Required(field.NewPath("webhook"), "required when type=Webhook")},
  1848  			knownTypes:      sets.NewString(string("Webhook")),
  1849  			repeatableTypes: sets.NewString(string("Webhook")),
  1850  		},
  1851  		{
  1852  			name: "when type!=Webhook, webhooks needs to be nil",
  1853  			configuration: api.AuthorizationConfiguration{
  1854  				Authorizers: []api.AuthorizerConfiguration{
  1855  					{
  1856  						Type:    "Foo",
  1857  						Name:    "foo",
  1858  						Webhook: &api.WebhookConfiguration{},
  1859  					},
  1860  				},
  1861  			},
  1862  			expectedErrList: field.ErrorList{field.Invalid(field.NewPath("webhook"), "non-null", "may only be specified when type=Webhook")},
  1863  			knownTypes:      sets.NewString(string("Foo")),
  1864  			repeatableTypes: sets.NewString(string("Webhook")),
  1865  		},
  1866  		{
  1867  			name: "timeout should be specified",
  1868  			configuration: api.AuthorizationConfiguration{
  1869  				Authorizers: []api.AuthorizerConfiguration{
  1870  					{
  1871  						Type: "Webhook",
  1872  						Name: "default",
  1873  						Webhook: &api.WebhookConfiguration{
  1874  							FailurePolicy:                            "NoOpinion",
  1875  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  1876  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1877  							SubjectAccessReviewVersion:               "v1",
  1878  							MatchConditionSubjectAccessReviewVersion: "v1",
  1879  							ConnectionInfo: api.WebhookConnectionInfo{
  1880  								Type: "InClusterConfig",
  1881  							},
  1882  						},
  1883  					},
  1884  				},
  1885  			},
  1886  			expectedErrList: field.ErrorList{field.Required(field.NewPath("timeout"), "")},
  1887  			knownTypes:      sets.NewString(string("Webhook")),
  1888  			repeatableTypes: sets.NewString(string("Webhook")),
  1889  		},
  1890  		//
  1891  		{
  1892  			name: "timeout shouldn't be zero",
  1893  			configuration: api.AuthorizationConfiguration{
  1894  				Authorizers: []api.AuthorizerConfiguration{
  1895  					{
  1896  						Type: "Webhook",
  1897  						Name: "default",
  1898  						Webhook: &api.WebhookConfiguration{
  1899  							FailurePolicy:                            "NoOpinion",
  1900  							Timeout:                                  metav1.Duration{Duration: 0 * time.Second},
  1901  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  1902  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1903  							SubjectAccessReviewVersion:               "v1",
  1904  							MatchConditionSubjectAccessReviewVersion: "v1",
  1905  							ConnectionInfo: api.WebhookConnectionInfo{
  1906  								Type: "InClusterConfig",
  1907  							},
  1908  						},
  1909  					},
  1910  				},
  1911  			},
  1912  			expectedErrList: field.ErrorList{field.Required(field.NewPath("timeout"), "")},
  1913  			knownTypes:      sets.NewString(string("Webhook")),
  1914  			repeatableTypes: sets.NewString(string("Webhook")),
  1915  		},
  1916  		{
  1917  			name: "timeout shouldn't be negative",
  1918  			configuration: api.AuthorizationConfiguration{
  1919  				Authorizers: []api.AuthorizerConfiguration{
  1920  					{
  1921  						Type: "Webhook",
  1922  						Name: "default",
  1923  						Webhook: &api.WebhookConfiguration{
  1924  							FailurePolicy:                            "NoOpinion",
  1925  							Timeout:                                  metav1.Duration{Duration: -30 * time.Second},
  1926  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  1927  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1928  							SubjectAccessReviewVersion:               "v1",
  1929  							MatchConditionSubjectAccessReviewVersion: "v1",
  1930  							ConnectionInfo: api.WebhookConnectionInfo{
  1931  								Type: "InClusterConfig",
  1932  							},
  1933  						},
  1934  					},
  1935  				},
  1936  			},
  1937  			expectedErrList: field.ErrorList{field.Invalid(field.NewPath("timeout"), time.Duration(-30*time.Second).String(), "must be > 0s and <= 30s")},
  1938  			knownTypes:      sets.NewString(string("Webhook")),
  1939  			repeatableTypes: sets.NewString(string("Webhook")),
  1940  		},
  1941  		{
  1942  			name: "timeout shouldn't be greater than 30seconds",
  1943  			configuration: api.AuthorizationConfiguration{
  1944  				Authorizers: []api.AuthorizerConfiguration{
  1945  					{
  1946  						Type: "Webhook",
  1947  						Name: "default",
  1948  						Webhook: &api.WebhookConfiguration{
  1949  							FailurePolicy:                            "NoOpinion",
  1950  							Timeout:                                  metav1.Duration{Duration: 60 * time.Second},
  1951  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  1952  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1953  							SubjectAccessReviewVersion:               "v1",
  1954  							MatchConditionSubjectAccessReviewVersion: "v1",
  1955  							ConnectionInfo: api.WebhookConnectionInfo{
  1956  								Type: "InClusterConfig",
  1957  							},
  1958  						},
  1959  					},
  1960  				},
  1961  			},
  1962  			expectedErrList: field.ErrorList{field.Invalid(field.NewPath("timeout"), time.Duration(60*time.Second).String(), "must be > 0s and <= 30s")},
  1963  			knownTypes:      sets.NewString(string("Webhook")),
  1964  			repeatableTypes: sets.NewString(string("Webhook")),
  1965  		},
  1966  		{
  1967  			name: "authorizedTTL should be defined ",
  1968  			configuration: api.AuthorizationConfiguration{
  1969  				Authorizers: []api.AuthorizerConfiguration{
  1970  					{
  1971  						Type: "Webhook",
  1972  						Name: "default",
  1973  						Webhook: &api.WebhookConfiguration{
  1974  							FailurePolicy:                            "NoOpinion",
  1975  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  1976  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  1977  							SubjectAccessReviewVersion:               "v1",
  1978  							MatchConditionSubjectAccessReviewVersion: "v1",
  1979  							ConnectionInfo: api.WebhookConnectionInfo{
  1980  								Type: "InClusterConfig",
  1981  							},
  1982  						},
  1983  					},
  1984  				},
  1985  			},
  1986  			expectedErrList: field.ErrorList{field.Required(field.NewPath("authorizedTTL"), "")},
  1987  			knownTypes:      sets.NewString(string("Webhook")),
  1988  			repeatableTypes: sets.NewString(string("Webhook")),
  1989  		},
  1990  		{
  1991  			name: "authorizedTTL shouldn't be negative",
  1992  			configuration: api.AuthorizationConfiguration{
  1993  				Authorizers: []api.AuthorizerConfiguration{
  1994  					{
  1995  						Type: "Webhook",
  1996  						Name: "default",
  1997  						Webhook: &api.WebhookConfiguration{
  1998  							FailurePolicy:                            "NoOpinion",
  1999  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2000  							AuthorizedTTL:                            metav1.Duration{Duration: -30 * time.Second},
  2001  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2002  							SubjectAccessReviewVersion:               "v1",
  2003  							MatchConditionSubjectAccessReviewVersion: "v1",
  2004  							ConnectionInfo: api.WebhookConnectionInfo{
  2005  								Type: "InClusterConfig",
  2006  							},
  2007  						},
  2008  					},
  2009  				},
  2010  			},
  2011  			expectedErrList: field.ErrorList{field.Invalid(field.NewPath("authorizedTTL"), time.Duration(-30*time.Second).String(), "must be > 0s")},
  2012  			knownTypes:      sets.NewString(string("Webhook")),
  2013  			repeatableTypes: sets.NewString(string("Webhook")),
  2014  		},
  2015  		{
  2016  			name: "unauthorizedTTL should be defined ",
  2017  			configuration: api.AuthorizationConfiguration{
  2018  				Authorizers: []api.AuthorizerConfiguration{
  2019  					{
  2020  						Type: "Webhook",
  2021  						Name: "default",
  2022  						Webhook: &api.WebhookConfiguration{
  2023  							FailurePolicy:                            "NoOpinion",
  2024  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2025  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2026  							SubjectAccessReviewVersion:               "v1",
  2027  							MatchConditionSubjectAccessReviewVersion: "v1",
  2028  							ConnectionInfo: api.WebhookConnectionInfo{
  2029  								Type: "InClusterConfig",
  2030  							},
  2031  						},
  2032  					},
  2033  				},
  2034  			},
  2035  			expectedErrList: field.ErrorList{field.Required(field.NewPath("unauthorizedTTL"), "")},
  2036  			knownTypes:      sets.NewString(string("Webhook")),
  2037  			repeatableTypes: sets.NewString(string("Webhook")),
  2038  		},
  2039  		{
  2040  			name: "unauthorizedTTL shouldn't be negative",
  2041  			configuration: api.AuthorizationConfiguration{
  2042  				Authorizers: []api.AuthorizerConfiguration{
  2043  					{
  2044  						Type: "Webhook",
  2045  						Name: "default",
  2046  						Webhook: &api.WebhookConfiguration{
  2047  							FailurePolicy:                            "NoOpinion",
  2048  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2049  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2050  							UnauthorizedTTL:                          metav1.Duration{Duration: -30 * time.Second},
  2051  							SubjectAccessReviewVersion:               "v1",
  2052  							MatchConditionSubjectAccessReviewVersion: "v1",
  2053  							ConnectionInfo: api.WebhookConnectionInfo{
  2054  								Type: "InClusterConfig",
  2055  							},
  2056  						},
  2057  					},
  2058  				},
  2059  			},
  2060  			expectedErrList: field.ErrorList{field.Invalid(field.NewPath("unauthorizedTTL"), time.Duration(-30*time.Second).String(), "must be > 0s")},
  2061  			knownTypes:      sets.NewString(string("Webhook")),
  2062  			repeatableTypes: sets.NewString(string("Webhook")),
  2063  		},
  2064  		{
  2065  			name: "SAR should be defined",
  2066  			configuration: api.AuthorizationConfiguration{
  2067  				Authorizers: []api.AuthorizerConfiguration{
  2068  					{
  2069  						Type: "Webhook",
  2070  						Name: "default",
  2071  						Webhook: &api.WebhookConfiguration{
  2072  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2073  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2074  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2075  							MatchConditionSubjectAccessReviewVersion: "v1",
  2076  							FailurePolicy:                            "NoOpinion",
  2077  							ConnectionInfo: api.WebhookConnectionInfo{
  2078  								Type: "InClusterConfig",
  2079  							},
  2080  						},
  2081  					},
  2082  				},
  2083  			},
  2084  			expectedErrList: field.ErrorList{field.Required(field.NewPath("subjectAccessReviewVersion"), "")},
  2085  			knownTypes:      sets.NewString(string("Webhook")),
  2086  			repeatableTypes: sets.NewString(string("Webhook")),
  2087  		},
  2088  		{
  2089  			name: "SAR should be one of v1 and v1beta1",
  2090  			configuration: api.AuthorizationConfiguration{
  2091  				Authorizers: []api.AuthorizerConfiguration{
  2092  					{
  2093  						Type: "Webhook",
  2094  						Name: "default",
  2095  						Webhook: &api.WebhookConfiguration{
  2096  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2097  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2098  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2099  							FailurePolicy:                            "NoOpinion",
  2100  							SubjectAccessReviewVersion:               "v2beta1",
  2101  							MatchConditionSubjectAccessReviewVersion: "v1",
  2102  							ConnectionInfo: api.WebhookConnectionInfo{
  2103  								Type: "InClusterConfig",
  2104  							},
  2105  						},
  2106  					},
  2107  				},
  2108  			},
  2109  			expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("subjectAccessReviewVersion"), "v2beta1", []string{"v1", "v1beta1"})},
  2110  			knownTypes:      sets.NewString(string("Webhook")),
  2111  			repeatableTypes: sets.NewString(string("Webhook")),
  2112  		},
  2113  		{
  2114  			name: "MatchConditionSAR should be defined",
  2115  			configuration: api.AuthorizationConfiguration{
  2116  				Authorizers: []api.AuthorizerConfiguration{
  2117  					{
  2118  						Type: "Webhook",
  2119  						Name: "default",
  2120  						Webhook: &api.WebhookConfiguration{
  2121  							Timeout:                    metav1.Duration{Duration: 5 * time.Second},
  2122  							AuthorizedTTL:              metav1.Duration{Duration: 5 * time.Minute},
  2123  							UnauthorizedTTL:            metav1.Duration{Duration: 30 * time.Second},
  2124  							FailurePolicy:              "NoOpinion",
  2125  							SubjectAccessReviewVersion: "v1",
  2126  							ConnectionInfo: api.WebhookConnectionInfo{
  2127  								Type: "InClusterConfig",
  2128  							},
  2129  							MatchConditions: []api.WebhookMatchCondition{{Expression: "true"}},
  2130  						},
  2131  					},
  2132  				},
  2133  			},
  2134  			expectedErrList: field.ErrorList{field.Required(field.NewPath("matchConditionSubjectAccessReviewVersion"), "")},
  2135  			knownTypes:      sets.NewString(string("Webhook")),
  2136  			repeatableTypes: sets.NewString(string("Webhook")),
  2137  		},
  2138  		{
  2139  			name: "MatchConditionSAR must not be anything other than v1",
  2140  			configuration: api.AuthorizationConfiguration{
  2141  				Authorizers: []api.AuthorizerConfiguration{
  2142  					{
  2143  						Type: "Webhook",
  2144  						Name: "default",
  2145  						Webhook: &api.WebhookConfiguration{
  2146  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2147  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2148  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2149  							FailurePolicy:                            "NoOpinion",
  2150  							SubjectAccessReviewVersion:               "v1",
  2151  							MatchConditionSubjectAccessReviewVersion: "v1beta1",
  2152  							ConnectionInfo: api.WebhookConnectionInfo{
  2153  								Type: "InClusterConfig",
  2154  							},
  2155  						},
  2156  					},
  2157  				},
  2158  			},
  2159  			expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("matchConditionSubjectAccessReviewVersion"), "v1beta1", []string{"v1"})},
  2160  			knownTypes:      sets.NewString(string("Webhook")),
  2161  			repeatableTypes: sets.NewString(string("Webhook")),
  2162  		},
  2163  		{
  2164  			name: "failurePolicy should be defined",
  2165  			configuration: api.AuthorizationConfiguration{
  2166  				Authorizers: []api.AuthorizerConfiguration{
  2167  					{
  2168  						Type: "Webhook",
  2169  						Name: "default",
  2170  						Webhook: &api.WebhookConfiguration{
  2171  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2172  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2173  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2174  							SubjectAccessReviewVersion:               "v1",
  2175  							MatchConditionSubjectAccessReviewVersion: "v1",
  2176  							ConnectionInfo: api.WebhookConnectionInfo{
  2177  								Type: "InClusterConfig",
  2178  							},
  2179  						},
  2180  					},
  2181  				},
  2182  			},
  2183  			expectedErrList: field.ErrorList{field.Required(field.NewPath("failurePolicy"), "")},
  2184  			knownTypes:      sets.NewString(string("Webhook")),
  2185  			repeatableTypes: sets.NewString(string("Webhook")),
  2186  		},
  2187  		{
  2188  			name: "failurePolicy should be one of \"NoOpinion\" or \"Deny\"",
  2189  			configuration: api.AuthorizationConfiguration{
  2190  				Authorizers: []api.AuthorizerConfiguration{
  2191  					{
  2192  						Type: "Webhook",
  2193  						Name: "default",
  2194  						Webhook: &api.WebhookConfiguration{
  2195  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2196  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2197  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2198  							FailurePolicy:                            "AlwaysAllow",
  2199  							SubjectAccessReviewVersion:               "v1",
  2200  							MatchConditionSubjectAccessReviewVersion: "v1",
  2201  							ConnectionInfo: api.WebhookConnectionInfo{
  2202  								Type: "InClusterConfig",
  2203  							},
  2204  						},
  2205  					},
  2206  				},
  2207  			},
  2208  			expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("failurePolicy"), "AlwaysAllow", []string{"NoOpinion", "Deny"})},
  2209  			knownTypes:      sets.NewString(string("Webhook")),
  2210  			repeatableTypes: sets.NewString(string("Webhook")),
  2211  		},
  2212  		{
  2213  			name: "connectionInfo should be defined",
  2214  			configuration: api.AuthorizationConfiguration{
  2215  				Authorizers: []api.AuthorizerConfiguration{
  2216  					{
  2217  						Type: "Webhook",
  2218  						Name: "default",
  2219  						Webhook: &api.WebhookConfiguration{
  2220  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2221  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2222  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2223  							FailurePolicy:                            "NoOpinion",
  2224  							SubjectAccessReviewVersion:               "v1",
  2225  							MatchConditionSubjectAccessReviewVersion: "v1",
  2226  						},
  2227  					},
  2228  				},
  2229  			},
  2230  			expectedErrList: field.ErrorList{field.Required(field.NewPath("connectionInfo"), "")},
  2231  			knownTypes:      sets.NewString(string("Webhook")),
  2232  			repeatableTypes: sets.NewString(string("Webhook")),
  2233  		},
  2234  		{
  2235  			name: "connectionInfo should be one of InClusterConfig or KubeConfigFile",
  2236  			configuration: api.AuthorizationConfiguration{
  2237  				Authorizers: []api.AuthorizerConfiguration{
  2238  					{
  2239  						Type: "Webhook",
  2240  						Name: "default",
  2241  						Webhook: &api.WebhookConfiguration{
  2242  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2243  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2244  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2245  							FailurePolicy:                            "NoOpinion",
  2246  							SubjectAccessReviewVersion:               "v1",
  2247  							MatchConditionSubjectAccessReviewVersion: "v1",
  2248  							ConnectionInfo: api.WebhookConnectionInfo{
  2249  								Type: "ExternalClusterConfig",
  2250  							},
  2251  						},
  2252  					},
  2253  				},
  2254  			},
  2255  			expectedErrList: field.ErrorList{
  2256  				field.NotSupported(field.NewPath("connectionInfo"), api.WebhookConnectionInfo{Type: "ExternalClusterConfig"}, []string{"InClusterConfig", "KubeConfigFile"}),
  2257  			},
  2258  			knownTypes:      sets.NewString(string("Webhook")),
  2259  			repeatableTypes: sets.NewString(string("Webhook")),
  2260  		},
  2261  		{
  2262  			name: "if connectionInfo=InClusterConfig, then kubeConfigFile should be nil",
  2263  			configuration: api.AuthorizationConfiguration{
  2264  				Authorizers: []api.AuthorizerConfiguration{
  2265  					{
  2266  						Type: "Webhook",
  2267  						Name: "default",
  2268  						Webhook: &api.WebhookConfiguration{
  2269  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2270  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2271  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2272  							FailurePolicy:                            "NoOpinion",
  2273  							SubjectAccessReviewVersion:               "v1",
  2274  							MatchConditionSubjectAccessReviewVersion: "v1",
  2275  							ConnectionInfo: api.WebhookConnectionInfo{
  2276  								Type:           "InClusterConfig",
  2277  								KubeConfigFile: new(string),
  2278  							},
  2279  						},
  2280  					},
  2281  				},
  2282  			},
  2283  			expectedErrList: field.ErrorList{
  2284  				field.Invalid(field.NewPath("connectionInfo", "kubeConfigFile"), "", "can only be set when type=KubeConfigFile"),
  2285  			},
  2286  			knownTypes:      sets.NewString(string("Webhook")),
  2287  			repeatableTypes: sets.NewString(string("Webhook")),
  2288  		},
  2289  		{
  2290  			name: "if connectionInfo=KubeConfigFile, then KubeConfigFile should be defined",
  2291  			configuration: api.AuthorizationConfiguration{
  2292  				Authorizers: []api.AuthorizerConfiguration{
  2293  					{
  2294  						Type: "Webhook",
  2295  						Name: "default",
  2296  						Webhook: &api.WebhookConfiguration{
  2297  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2298  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2299  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2300  							FailurePolicy:                            "NoOpinion",
  2301  							SubjectAccessReviewVersion:               "v1",
  2302  							MatchConditionSubjectAccessReviewVersion: "v1",
  2303  							ConnectionInfo: api.WebhookConnectionInfo{
  2304  								Type: "KubeConfigFile",
  2305  							},
  2306  						},
  2307  					},
  2308  				},
  2309  			},
  2310  			expectedErrList: field.ErrorList{field.Required(field.NewPath("kubeConfigFile"), "")},
  2311  			knownTypes:      sets.NewString(string("Webhook")),
  2312  			repeatableTypes: sets.NewString(string("Webhook")),
  2313  		},
  2314  		{
  2315  			name: "if connectionInfo=KubeConfigFile, then KubeConfigFile should be defined, must be an absolute path, should exist, shouldn't be a symlink",
  2316  			configuration: api.AuthorizationConfiguration{
  2317  				Authorizers: []api.AuthorizerConfiguration{
  2318  					{
  2319  						Type: "Webhook",
  2320  						Name: "default",
  2321  						Webhook: &api.WebhookConfiguration{
  2322  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2323  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2324  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2325  							FailurePolicy:                            "NoOpinion",
  2326  							SubjectAccessReviewVersion:               "v1",
  2327  							MatchConditionSubjectAccessReviewVersion: "v1",
  2328  							ConnectionInfo: api.WebhookConnectionInfo{
  2329  								Type:           "KubeConfigFile",
  2330  								KubeConfigFile: &badKubeConfigFile,
  2331  							},
  2332  						},
  2333  					},
  2334  				},
  2335  			},
  2336  			expectedErrList: field.ErrorList{field.Invalid(field.NewPath("kubeConfigFile"), badKubeConfigFile, "must be an absolute path")},
  2337  			knownTypes:      sets.NewString(string("Webhook")),
  2338  			repeatableTypes: sets.NewString(string("Webhook")),
  2339  		},
  2340  		{
  2341  			name: "if connectionInfo=KubeConfigFile, an existent file needs to be passed",
  2342  			configuration: api.AuthorizationConfiguration{
  2343  				Authorizers: []api.AuthorizerConfiguration{
  2344  					{
  2345  						Type: "Webhook",
  2346  						Name: "default",
  2347  						Webhook: &api.WebhookConfiguration{
  2348  							Timeout:                                  metav1.Duration{Duration: 5 * time.Second},
  2349  							AuthorizedTTL:                            metav1.Duration{Duration: 5 * time.Minute},
  2350  							UnauthorizedTTL:                          metav1.Duration{Duration: 30 * time.Second},
  2351  							FailurePolicy:                            "NoOpinion",
  2352  							SubjectAccessReviewVersion:               "v1",
  2353  							MatchConditionSubjectAccessReviewVersion: "v1",
  2354  							ConnectionInfo: api.WebhookConnectionInfo{
  2355  								Type:           "KubeConfigFile",
  2356  								KubeConfigFile: &tempKubeConfigFilePath,
  2357  							},
  2358  						},
  2359  					},
  2360  				},
  2361  			},
  2362  			expectedErrList: field.ErrorList{},
  2363  			knownTypes:      sets.NewString(string("Webhook")),
  2364  			repeatableTypes: sets.NewString(string("Webhook")),
  2365  		},
  2366  	}
  2367  
  2368  	for _, test := range tests {
  2369  		t.Run(test.name, func(t *testing.T) {
  2370  			errList := ValidateAuthorizationConfiguration(nil, &test.configuration, test.knownTypes, test.repeatableTypes)
  2371  			if len(errList) != len(test.expectedErrList) {
  2372  				t.Errorf("expected %d errs, got %d, errors %v", len(test.expectedErrList), len(errList), errList)
  2373  			}
  2374  			if len(errList) == len(test.expectedErrList) {
  2375  				for i, expected := range test.expectedErrList {
  2376  					if expected.Type.String() != errList[i].Type.String() {
  2377  						t.Errorf("expected err type %s, got %s",
  2378  							expected.Type.String(),
  2379  							errList[i].Type.String())
  2380  					}
  2381  					if expected.BadValue != errList[i].BadValue {
  2382  						t.Errorf("expected bad value '%s', got '%s'",
  2383  							expected.BadValue,
  2384  							errList[i].BadValue)
  2385  					}
  2386  				}
  2387  			}
  2388  		})
  2389  
  2390  	}
  2391  }
  2392  
  2393  func TestValidateAndCompileMatchConditions(t *testing.T) {
  2394  	testCases := []struct {
  2395  		name            string
  2396  		matchConditions []api.WebhookMatchCondition
  2397  		featureEnabled  bool
  2398  		expectedErr     string
  2399  	}{
  2400  		{
  2401  			name: "match conditions are used With feature enabled",
  2402  			matchConditions: []api.WebhookMatchCondition{
  2403  				{
  2404  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
  2405  				},
  2406  				{
  2407  					Expression: "request.user == 'admin'",
  2408  				},
  2409  			},
  2410  			featureEnabled: true,
  2411  			expectedErr:    "",
  2412  		},
  2413  		{
  2414  			name: "should fail when match conditions are used without feature enabled",
  2415  			matchConditions: []api.WebhookMatchCondition{
  2416  				{
  2417  					Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
  2418  				},
  2419  				{
  2420  					Expression: "request.user == 'admin'",
  2421  				},
  2422  			},
  2423  			featureEnabled: false,
  2424  			expectedErr:    `matchConditions: Invalid value: "": matchConditions are not supported when StructuredAuthorizationConfiguration feature gate is disabled`,
  2425  		},
  2426  		{
  2427  			name:            "no matchConditions should not require feature enablement",
  2428  			matchConditions: []api.WebhookMatchCondition{},
  2429  			featureEnabled:  false,
  2430  			expectedErr:     "",
  2431  		},
  2432  		{
  2433  			name: "match conditions with invalid expressions",
  2434  			matchConditions: []api.WebhookMatchCondition{
  2435  				{
  2436  					Expression: "  ",
  2437  				},
  2438  			},
  2439  			featureEnabled: true,
  2440  			expectedErr:    "matchConditions[0].expression: Required value",
  2441  		},
  2442  		{
  2443  			name: "match conditions with duplicate expressions",
  2444  			matchConditions: []api.WebhookMatchCondition{
  2445  				{
  2446  					Expression: "request.user == 'admin'",
  2447  				},
  2448  				{
  2449  					Expression: "request.user == 'admin'",
  2450  				},
  2451  			},
  2452  			featureEnabled: true,
  2453  			expectedErr:    `matchConditions[1].expression: Duplicate value: "request.user == 'admin'"`,
  2454  		},
  2455  		{
  2456  			name: "match conditions with undeclared reference",
  2457  			matchConditions: []api.WebhookMatchCondition{
  2458  				{
  2459  					Expression: "test",
  2460  				},
  2461  			},
  2462  			featureEnabled: true,
  2463  			expectedErr:    "matchConditions[0].expression: Invalid value: \"test\": compilation failed: ERROR: <input>:1:1: undeclared reference to 'test' (in container '')\n | test\n | ^",
  2464  		},
  2465  		{
  2466  			name: "match conditions with bad return type",
  2467  			matchConditions: []api.WebhookMatchCondition{
  2468  				{
  2469  					Expression: "request.user = 'test'",
  2470  				},
  2471  			},
  2472  			featureEnabled: true,
  2473  			expectedErr:    "matchConditions[0].expression: Invalid value: \"request.user = 'test'\": compilation failed: ERROR: <input>:1:14: Syntax error: token recognition error at: '= '\n | request.user = 'test'\n | .............^\nERROR: <input>:1:16: Syntax error: extraneous input ''test'' expecting <EOF>\n | request.user = 'test'\n | ...............^",
  2474  		},
  2475  	}
  2476  
  2477  	for _, tt := range testCases {
  2478  		t.Run(tt.name, func(t *testing.T) {
  2479  			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, tt.featureEnabled)
  2480  			celMatcher, errList := ValidateAndCompileMatchConditions(tt.matchConditions)
  2481  			if len(tt.expectedErr) == 0 && len(tt.matchConditions) > 0 && len(errList) == 0 && celMatcher == nil {
  2482  				t.Errorf("celMatcher should not be nil when there are matchCondition and no error returned")
  2483  			}
  2484  			got := errList.ToAggregate()
  2485  			if d := cmp.Diff(tt.expectedErr, errString(got)); d != "" {
  2486  				t.Fatalf("ValidateAndCompileMatchConditions validation mismatch (-want +got):\n%s", d)
  2487  			}
  2488  		})
  2489  	}
  2490  }