k8s.io/kubernetes@v1.29.3/pkg/kubeapiserver/options/authentication_test.go (about)

     1  /*
     2  Copyright 2018 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 options
    18  
    19  import (
    20  	"os"
    21  	"reflect"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/spf13/pflag"
    28  
    29  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    30  	"k8s.io/apimachinery/pkg/util/wait"
    31  	"k8s.io/apiserver/pkg/apis/apiserver"
    32  	"k8s.io/apiserver/pkg/authentication/authenticator"
    33  	"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
    34  	"k8s.io/apiserver/pkg/authentication/request/headerrequest"
    35  	"k8s.io/apiserver/pkg/features"
    36  	apiserveroptions "k8s.io/apiserver/pkg/server/options"
    37  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    38  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    39  	kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator"
    40  	"k8s.io/utils/pointer"
    41  )
    42  
    43  func TestAuthenticationValidate(t *testing.T) {
    44  	testCases := []struct {
    45  		name                         string
    46  		testOIDC                     *OIDCAuthenticationOptions
    47  		testSA                       *ServiceAccountAuthenticationOptions
    48  		testWebHook                  *WebHookAuthenticationOptions
    49  		testAuthenticationConfigFile string
    50  		expectErr                    string
    51  	}{
    52  		{
    53  			name: "test when OIDC and ServiceAccounts are nil",
    54  		},
    55  		{
    56  			name: "test when OIDC and ServiceAccounts are valid",
    57  			testOIDC: &OIDCAuthenticationOptions{
    58  				UsernameClaim:      "sub",
    59  				SigningAlgs:        []string{"RS256"},
    60  				IssuerURL:          "https://testIssuerURL",
    61  				ClientID:           "testClientID",
    62  				areFlagsConfigured: func() bool { return true },
    63  			},
    64  			testSA: &ServiceAccountAuthenticationOptions{
    65  				Issuers:  []string{"http://foo.bar.com"},
    66  				KeyFiles: []string{"testkeyfile1", "testkeyfile2"},
    67  			},
    68  		},
    69  		{
    70  			name: "test when OIDC is invalid",
    71  			testOIDC: &OIDCAuthenticationOptions{
    72  				UsernameClaim:      "sub",
    73  				SigningAlgs:        []string{"RS256"},
    74  				IssuerURL:          "https://testIssuerURL",
    75  				areFlagsConfigured: func() bool { return true },
    76  			},
    77  			testSA: &ServiceAccountAuthenticationOptions{
    78  				Issuers:  []string{"http://foo.bar.com"},
    79  				KeyFiles: []string{"testkeyfile1", "testkeyfile2"},
    80  			},
    81  			expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set",
    82  		},
    83  		{
    84  			name: "test when ServiceAccounts doesn't have key file",
    85  			testOIDC: &OIDCAuthenticationOptions{
    86  				UsernameClaim:      "sub",
    87  				SigningAlgs:        []string{"RS256"},
    88  				IssuerURL:          "https://testIssuerURL",
    89  				ClientID:           "testClientID",
    90  				areFlagsConfigured: func() bool { return true },
    91  			},
    92  			testSA: &ServiceAccountAuthenticationOptions{
    93  				Issuers: []string{"http://foo.bar.com"},
    94  			},
    95  			expectErr: "service-account-key-file is a required flag",
    96  		},
    97  		{
    98  			name: "test when ServiceAccounts doesn't have issuer",
    99  			testOIDC: &OIDCAuthenticationOptions{
   100  				UsernameClaim:      "sub",
   101  				SigningAlgs:        []string{"RS256"},
   102  				IssuerURL:          "https://testIssuerURL",
   103  				ClientID:           "testClientID",
   104  				areFlagsConfigured: func() bool { return true },
   105  			},
   106  			testSA: &ServiceAccountAuthenticationOptions{
   107  				Issuers: []string{},
   108  			},
   109  			expectErr: "service-account-issuer is a required flag",
   110  		},
   111  		{
   112  			name: "test when ServiceAccounts has empty string as issuer",
   113  			testOIDC: &OIDCAuthenticationOptions{
   114  				UsernameClaim:      "sub",
   115  				SigningAlgs:        []string{"RS256"},
   116  				IssuerURL:          "https://testIssuerURL",
   117  				ClientID:           "testClientID",
   118  				areFlagsConfigured: func() bool { return true },
   119  			},
   120  			testSA: &ServiceAccountAuthenticationOptions{
   121  				Issuers: []string{""},
   122  			},
   123  			expectErr: "service-account-issuer should not be an empty string",
   124  		},
   125  		{
   126  			name: "test when ServiceAccounts has duplicate issuers",
   127  			testOIDC: &OIDCAuthenticationOptions{
   128  				UsernameClaim:      "sub",
   129  				SigningAlgs:        []string{"RS256"},
   130  				IssuerURL:          "https://testIssuerURL",
   131  				ClientID:           "testClientID",
   132  				areFlagsConfigured: func() bool { return true },
   133  			},
   134  			testSA: &ServiceAccountAuthenticationOptions{
   135  				Issuers: []string{"http://foo.bar.com", "http://foo.bar.com"},
   136  			},
   137  			expectErr: "service-account-issuer \"http://foo.bar.com\" is already specified",
   138  		},
   139  		{
   140  			name: "test when ServiceAccount has bad issuer",
   141  			testOIDC: &OIDCAuthenticationOptions{
   142  				UsernameClaim:      "sub",
   143  				SigningAlgs:        []string{"RS256"},
   144  				IssuerURL:          "https://testIssuerURL",
   145  				ClientID:           "testClientID",
   146  				areFlagsConfigured: func() bool { return true },
   147  			},
   148  			testSA: &ServiceAccountAuthenticationOptions{
   149  				Issuers: []string{"http://[::1]:namedport"},
   150  			},
   151  			expectErr: "service-account-issuer \"http://[::1]:namedport\" contained a ':' but was not a valid URL",
   152  		},
   153  		{
   154  			name: "test when ServiceAccounts has invalid JWKSURI",
   155  			testOIDC: &OIDCAuthenticationOptions{
   156  				UsernameClaim:      "sub",
   157  				SigningAlgs:        []string{"RS256"},
   158  				IssuerURL:          "https://testIssuerURL",
   159  				ClientID:           "testClientID",
   160  				areFlagsConfigured: func() bool { return true },
   161  			},
   162  			testSA: &ServiceAccountAuthenticationOptions{
   163  				KeyFiles: []string{"cert", "key"},
   164  				Issuers:  []string{"http://foo.bar.com"},
   165  				JWKSURI:  "https://host:port",
   166  			},
   167  			expectErr: "service-account-jwks-uri must be a valid URL: parse \"https://host:port\": invalid port \":port\" after host",
   168  		},
   169  		{
   170  			name: "test when ServiceAccounts has invalid JWKSURI (not https scheme)",
   171  			testOIDC: &OIDCAuthenticationOptions{
   172  				UsernameClaim:      "sub",
   173  				SigningAlgs:        []string{"RS256"},
   174  				IssuerURL:          "https://testIssuerURL",
   175  				ClientID:           "testClientID",
   176  				areFlagsConfigured: func() bool { return true },
   177  			},
   178  			testSA: &ServiceAccountAuthenticationOptions{
   179  				KeyFiles: []string{"cert", "key"},
   180  				Issuers:  []string{"http://foo.bar.com"},
   181  				JWKSURI:  "http://baz.com",
   182  			},
   183  			expectErr: "service-account-jwks-uri requires https scheme, parsed as: http://baz.com",
   184  		},
   185  		{
   186  			name: "test when WebHook has invalid retry attempts",
   187  			testOIDC: &OIDCAuthenticationOptions{
   188  				UsernameClaim:      "sub",
   189  				SigningAlgs:        []string{"RS256"},
   190  				IssuerURL:          "https://testIssuerURL",
   191  				ClientID:           "testClientID",
   192  				areFlagsConfigured: func() bool { return true },
   193  			},
   194  			testSA: &ServiceAccountAuthenticationOptions{
   195  				KeyFiles: []string{"cert", "key"},
   196  				Issuers:  []string{"http://foo.bar.com"},
   197  				JWKSURI:  "https://baz.com",
   198  			},
   199  			testWebHook: &WebHookAuthenticationOptions{
   200  				ConfigFile: "configfile",
   201  				Version:    "v1",
   202  				CacheTTL:   60 * time.Second,
   203  				RetryBackoff: &wait.Backoff{
   204  					Duration: 500 * time.Millisecond,
   205  					Factor:   1.5,
   206  					Jitter:   0.2,
   207  					Steps:    0,
   208  				},
   209  			},
   210  			expectErr: "number of webhook retry attempts must be greater than 0, but is: 0",
   211  		},
   212  		{
   213  			name:                         "test when authentication config file is set without feature gate",
   214  			testAuthenticationConfigFile: "configfile",
   215  			expectErr:                    "set --feature-gates=StructuredAuthenticationConfiguration=true to use authentication-config file",
   216  		},
   217  		{
   218  			name:                         "test when authentication config file and oidc-* flags are set",
   219  			testAuthenticationConfigFile: "configfile",
   220  			testOIDC: &OIDCAuthenticationOptions{
   221  				UsernameClaim:      "sub",
   222  				SigningAlgs:        []string{"RS256"},
   223  				IssuerURL:          "https://testIssuerURL",
   224  				ClientID:           "testClientID",
   225  				areFlagsConfigured: func() bool { return true },
   226  			},
   227  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   228  		},
   229  	}
   230  
   231  	for _, testcase := range testCases {
   232  		t.Run(testcase.name, func(t *testing.T) {
   233  			options := NewBuiltInAuthenticationOptions()
   234  			options.OIDC = testcase.testOIDC
   235  			options.ServiceAccounts = testcase.testSA
   236  			options.WebHook = testcase.testWebHook
   237  			options.AuthenticationConfigFile = testcase.testAuthenticationConfigFile
   238  
   239  			errs := options.Validate()
   240  			if len(errs) > 0 && (!strings.Contains(utilerrors.NewAggregate(errs).Error(), testcase.expectErr) || testcase.expectErr == "") {
   241  				t.Errorf("Got err: %v, Expected err: %s", errs, testcase.expectErr)
   242  			}
   243  			if len(errs) == 0 && len(testcase.expectErr) != 0 {
   244  				t.Errorf("Got err nil, Expected err: %s", testcase.expectErr)
   245  			}
   246  		})
   247  	}
   248  }
   249  
   250  func TestToAuthenticationConfig(t *testing.T) {
   251  	testOptions := &BuiltInAuthenticationOptions{
   252  		Anonymous: &AnonymousAuthenticationOptions{
   253  			Allow: false,
   254  		},
   255  		ClientCert: &apiserveroptions.ClientCertAuthenticationOptions{
   256  			ClientCA: "testdata/root.pem",
   257  		},
   258  		WebHook: &WebHookAuthenticationOptions{
   259  			CacheTTL:   180000000000,
   260  			ConfigFile: "/token-webhook-config",
   261  		},
   262  		BootstrapToken: &BootstrapTokenAuthenticationOptions{
   263  			Enable: false,
   264  		},
   265  		OIDC: &OIDCAuthenticationOptions{
   266  			CAFile:        "testdata/root.pem",
   267  			UsernameClaim: "sub",
   268  			SigningAlgs:   []string{"RS256"},
   269  			IssuerURL:     "https://testIssuerURL",
   270  			ClientID:      "testClientID",
   271  		},
   272  		RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{
   273  			UsernameHeaders:     []string{"x-remote-user"},
   274  			GroupHeaders:        []string{"x-remote-group"},
   275  			ExtraHeaderPrefixes: []string{"x-remote-extra-"},
   276  			ClientCAFile:        "testdata/root.pem",
   277  			AllowedNames:        []string{"kube-aggregator"},
   278  		},
   279  		ServiceAccounts: &ServiceAccountAuthenticationOptions{
   280  			Lookup:  true,
   281  			Issuers: []string{"http://foo.bar.com"},
   282  		},
   283  		TokenFile: &TokenFileAuthenticationOptions{
   284  			TokenFile: "/testTokenFile",
   285  		},
   286  		TokenSuccessCacheTTL: 10 * time.Second,
   287  		TokenFailureCacheTTL: 0,
   288  	}
   289  
   290  	expectConfig := kubeauthenticator.Config{
   291  		APIAudiences:            authenticator.Audiences{"http://foo.bar.com"},
   292  		Anonymous:               false,
   293  		BootstrapToken:          false,
   294  		ClientCAContentProvider: nil, // this is nil because you can't compare functions
   295  		TokenAuthFile:           "/testTokenFile",
   296  		AuthenticationConfig: &apiserver.AuthenticationConfiguration{
   297  			JWT: []apiserver.JWTAuthenticator{
   298  				{
   299  					Issuer: apiserver.Issuer{
   300  						URL:       "https://testIssuerURL",
   301  						Audiences: []string{"testClientID"},
   302  					},
   303  					ClaimMappings: apiserver.ClaimMappings{
   304  						Username: apiserver.PrefixedClaimOrExpression{
   305  							Claim:  "sub",
   306  							Prefix: pointer.String("https://testIssuerURL#"),
   307  						},
   308  					},
   309  				},
   310  			},
   311  		},
   312  		OIDCSigningAlgs:             []string{"RS256"},
   313  		ServiceAccountLookup:        true,
   314  		ServiceAccountIssuers:       []string{"http://foo.bar.com"},
   315  		WebhookTokenAuthnConfigFile: "/token-webhook-config",
   316  		WebhookTokenAuthnCacheTTL:   180000000000,
   317  
   318  		TokenSuccessCacheTTL: 10 * time.Second,
   319  		TokenFailureCacheTTL: 0,
   320  
   321  		RequestHeaderConfig: &authenticatorfactory.RequestHeaderConfig{
   322  			UsernameHeaders:     headerrequest.StaticStringSlice{"x-remote-user"},
   323  			GroupHeaders:        headerrequest.StaticStringSlice{"x-remote-group"},
   324  			ExtraHeaderPrefixes: headerrequest.StaticStringSlice{"x-remote-extra-"},
   325  			CAContentProvider:   nil, // this is nil because you can't compare functions
   326  			AllowedClientNames:  headerrequest.StaticStringSlice{"kube-aggregator"},
   327  		},
   328  	}
   329  
   330  	fileBytes, err := os.ReadFile("testdata/root.pem")
   331  	if err != nil {
   332  		t.Fatal(err)
   333  	}
   334  	expectConfig.AuthenticationConfig.JWT[0].Issuer.CertificateAuthority = string(fileBytes)
   335  
   336  	resultConfig, err := testOptions.ToAuthenticationConfig()
   337  	if err != nil {
   338  		t.Fatal(err)
   339  	}
   340  
   341  	// nil these out because you cannot compare pointers.  Ensure they are non-nil first
   342  	if resultConfig.ClientCAContentProvider == nil {
   343  		t.Error("missing client verify")
   344  	}
   345  	if resultConfig.RequestHeaderConfig.CAContentProvider == nil {
   346  		t.Error("missing requestheader verify")
   347  	}
   348  	resultConfig.ClientCAContentProvider = nil
   349  	resultConfig.RequestHeaderConfig.CAContentProvider = nil
   350  
   351  	if !reflect.DeepEqual(resultConfig, expectConfig) {
   352  		t.Error(cmp.Diff(resultConfig, expectConfig))
   353  	}
   354  }
   355  
   356  func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) {
   357  	var args = []string{
   358  		"--api-audiences=foo",
   359  		"--anonymous-auth=true",
   360  		"--enable-bootstrap-token-auth=true",
   361  		"--oidc-issuer-url=https://baz.com",
   362  		"--oidc-client-id=client-id",
   363  		"--oidc-ca-file=cert",
   364  		"--oidc-username-prefix=-",
   365  		"--client-ca-file=client-cacert",
   366  		"--requestheader-client-ca-file=testdata/root.pem",
   367  		"--requestheader-username-headers=x-remote-user-custom",
   368  		"--requestheader-group-headers=x-remote-group-custom",
   369  		"--requestheader-allowed-names=kube-aggregator",
   370  		"--service-account-key-file=cert",
   371  		"--service-account-key-file=key",
   372  		"--service-account-issuer=http://foo.bar.com",
   373  		"--service-account-jwks-uri=https://qux.com",
   374  		"--token-auth-file=tokenfile",
   375  		"--authentication-token-webhook-config-file=webhook_config.yaml",
   376  		"--authentication-token-webhook-cache-ttl=180s",
   377  	}
   378  
   379  	expected := &BuiltInAuthenticationOptions{
   380  		APIAudiences: []string{"foo"},
   381  		Anonymous: &AnonymousAuthenticationOptions{
   382  			Allow: true,
   383  		},
   384  		BootstrapToken: &BootstrapTokenAuthenticationOptions{
   385  			Enable: true,
   386  		},
   387  		ClientCert: &apiserveroptions.ClientCertAuthenticationOptions{
   388  			ClientCA: "client-cacert",
   389  		},
   390  		OIDC: &OIDCAuthenticationOptions{
   391  			CAFile:         "cert",
   392  			ClientID:       "client-id",
   393  			IssuerURL:      "https://baz.com",
   394  			UsernameClaim:  "sub",
   395  			UsernamePrefix: "-",
   396  			SigningAlgs:    []string{"RS256"},
   397  		},
   398  		RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{
   399  			ClientCAFile:    "testdata/root.pem",
   400  			UsernameHeaders: []string{"x-remote-user-custom"},
   401  			GroupHeaders:    []string{"x-remote-group-custom"},
   402  			AllowedNames:    []string{"kube-aggregator"},
   403  		},
   404  		ServiceAccounts: &ServiceAccountAuthenticationOptions{
   405  			KeyFiles:         []string{"cert", "key"},
   406  			Lookup:           true,
   407  			Issuers:          []string{"http://foo.bar.com"},
   408  			JWKSURI:          "https://qux.com",
   409  			ExtendExpiration: true,
   410  		},
   411  		TokenFile: &TokenFileAuthenticationOptions{
   412  			TokenFile: "tokenfile",
   413  		},
   414  		WebHook: &WebHookAuthenticationOptions{
   415  			ConfigFile: "webhook_config.yaml",
   416  			Version:    "v1beta1",
   417  			CacheTTL:   180 * time.Second,
   418  			RetryBackoff: &wait.Backoff{
   419  				Duration: 500 * time.Millisecond,
   420  				Factor:   1.5,
   421  				Jitter:   0.2,
   422  				Steps:    5,
   423  			},
   424  		},
   425  		TokenSuccessCacheTTL: 10 * time.Second,
   426  		TokenFailureCacheTTL: 0 * time.Second,
   427  	}
   428  
   429  	opts := NewBuiltInAuthenticationOptions().WithAll()
   430  	pf := pflag.NewFlagSet("test-builtin-authentication-opts", pflag.ContinueOnError)
   431  	opts.AddFlags(pf)
   432  
   433  	if err := pf.Parse(args); err != nil {
   434  		t.Fatal(err)
   435  	}
   436  
   437  	if !opts.OIDC.areFlagsConfigured() {
   438  		t.Fatal("OIDC flags should be configured")
   439  	}
   440  	// nil these out because you cannot compare functions
   441  	opts.OIDC.areFlagsConfigured = nil
   442  
   443  	if !reflect.DeepEqual(opts, expected) {
   444  		t.Error(cmp.Diff(opts, expected, cmp.AllowUnexported(OIDCAuthenticationOptions{})))
   445  	}
   446  }
   447  
   448  func TestToAuthenticationConfig_OIDC(t *testing.T) {
   449  	testCases := []struct {
   450  		name         string
   451  		args         []string
   452  		expectConfig kubeauthenticator.Config
   453  	}{
   454  		{
   455  			name: "username prefix is '-'",
   456  			args: []string{
   457  				"--oidc-issuer-url=https://testIssuerURL",
   458  				"--oidc-client-id=testClientID",
   459  				"--oidc-username-claim=sub",
   460  				"--oidc-username-prefix=-",
   461  				"--oidc-signing-algs=RS256",
   462  				"--oidc-required-claim=foo=bar",
   463  			},
   464  			expectConfig: kubeauthenticator.Config{
   465  				TokenSuccessCacheTTL: 10 * time.Second,
   466  				AuthenticationConfig: &apiserver.AuthenticationConfiguration{
   467  					JWT: []apiserver.JWTAuthenticator{
   468  						{
   469  							Issuer: apiserver.Issuer{
   470  								URL:       "https://testIssuerURL",
   471  								Audiences: []string{"testClientID"},
   472  							},
   473  							ClaimMappings: apiserver.ClaimMappings{
   474  								Username: apiserver.PrefixedClaimOrExpression{
   475  									Claim:  "sub",
   476  									Prefix: pointer.String(""),
   477  								},
   478  							},
   479  							ClaimValidationRules: []apiserver.ClaimValidationRule{
   480  								{
   481  									Claim:         "foo",
   482  									RequiredValue: "bar",
   483  								},
   484  							},
   485  						},
   486  					},
   487  				},
   488  				OIDCSigningAlgs: []string{"RS256"},
   489  			},
   490  		},
   491  		{
   492  			name: "--oidc-username-prefix is empty, --oidc-username-claim is not email",
   493  			args: []string{
   494  				"--oidc-issuer-url=https://testIssuerURL",
   495  				"--oidc-client-id=testClientID",
   496  				"--oidc-username-claim=sub",
   497  				"--oidc-signing-algs=RS256",
   498  				"--oidc-required-claim=foo=bar",
   499  			},
   500  			expectConfig: kubeauthenticator.Config{
   501  				TokenSuccessCacheTTL: 10 * time.Second,
   502  				AuthenticationConfig: &apiserver.AuthenticationConfiguration{
   503  					JWT: []apiserver.JWTAuthenticator{
   504  						{
   505  							Issuer: apiserver.Issuer{
   506  								URL:       "https://testIssuerURL",
   507  								Audiences: []string{"testClientID"},
   508  							},
   509  							ClaimMappings: apiserver.ClaimMappings{
   510  								Username: apiserver.PrefixedClaimOrExpression{
   511  									Claim:  "sub",
   512  									Prefix: pointer.String("https://testIssuerURL#"),
   513  								},
   514  							},
   515  							ClaimValidationRules: []apiserver.ClaimValidationRule{
   516  								{
   517  									Claim:         "foo",
   518  									RequiredValue: "bar",
   519  								},
   520  							},
   521  						},
   522  					},
   523  				},
   524  				OIDCSigningAlgs: []string{"RS256"},
   525  			},
   526  		},
   527  		{
   528  			name: "--oidc-username-prefix is empty, --oidc-username-claim is email",
   529  			args: []string{
   530  				"--oidc-issuer-url=https://testIssuerURL",
   531  				"--oidc-client-id=testClientID",
   532  				"--oidc-username-claim=email",
   533  				"--oidc-signing-algs=RS256",
   534  				"--oidc-required-claim=foo=bar",
   535  			},
   536  			expectConfig: kubeauthenticator.Config{
   537  				TokenSuccessCacheTTL: 10 * time.Second,
   538  				AuthenticationConfig: &apiserver.AuthenticationConfiguration{
   539  					JWT: []apiserver.JWTAuthenticator{
   540  						{
   541  							Issuer: apiserver.Issuer{
   542  								URL:       "https://testIssuerURL",
   543  								Audiences: []string{"testClientID"},
   544  							},
   545  							ClaimMappings: apiserver.ClaimMappings{
   546  								Username: apiserver.PrefixedClaimOrExpression{
   547  									Claim:  "email",
   548  									Prefix: pointer.String(""),
   549  								},
   550  							},
   551  							ClaimValidationRules: []apiserver.ClaimValidationRule{
   552  								{
   553  									Claim:         "foo",
   554  									RequiredValue: "bar",
   555  								},
   556  							},
   557  						},
   558  					},
   559  				},
   560  				OIDCSigningAlgs: []string{"RS256"},
   561  			},
   562  		},
   563  		{
   564  			name: "non empty username prefix",
   565  			args: []string{
   566  				"--oidc-issuer-url=https://testIssuerURL",
   567  				"--oidc-client-id=testClientID",
   568  				"--oidc-username-claim=sub",
   569  				"--oidc-username-prefix=k8s-",
   570  				"--oidc-signing-algs=RS256",
   571  				"--oidc-required-claim=foo=bar",
   572  			},
   573  			expectConfig: kubeauthenticator.Config{
   574  				TokenSuccessCacheTTL: 10 * time.Second,
   575  				AuthenticationConfig: &apiserver.AuthenticationConfiguration{
   576  					JWT: []apiserver.JWTAuthenticator{
   577  						{
   578  							Issuer: apiserver.Issuer{
   579  								URL:       "https://testIssuerURL",
   580  								Audiences: []string{"testClientID"},
   581  							},
   582  							ClaimMappings: apiserver.ClaimMappings{
   583  								Username: apiserver.PrefixedClaimOrExpression{
   584  									Claim:  "sub",
   585  									Prefix: pointer.String("k8s-"),
   586  								},
   587  							},
   588  							ClaimValidationRules: []apiserver.ClaimValidationRule{
   589  								{
   590  									Claim:         "foo",
   591  									RequiredValue: "bar",
   592  								},
   593  							},
   594  						},
   595  					},
   596  				},
   597  				OIDCSigningAlgs: []string{"RS256"},
   598  			},
   599  		},
   600  		{
   601  			name: "groups claim exists",
   602  			args: []string{
   603  				"--oidc-issuer-url=https://testIssuerURL",
   604  				"--oidc-client-id=testClientID",
   605  				"--oidc-username-claim=sub",
   606  				"--oidc-username-prefix=-",
   607  				"--oidc-groups-claim=groups",
   608  				"--oidc-groups-prefix=oidc:",
   609  				"--oidc-signing-algs=RS256",
   610  				"--oidc-required-claim=foo=bar",
   611  			},
   612  			expectConfig: kubeauthenticator.Config{
   613  				TokenSuccessCacheTTL: 10 * time.Second,
   614  				AuthenticationConfig: &apiserver.AuthenticationConfiguration{
   615  					JWT: []apiserver.JWTAuthenticator{
   616  						{
   617  							Issuer: apiserver.Issuer{
   618  								URL:       "https://testIssuerURL",
   619  								Audiences: []string{"testClientID"},
   620  							},
   621  							ClaimMappings: apiserver.ClaimMappings{
   622  								Username: apiserver.PrefixedClaimOrExpression{
   623  									Claim:  "sub",
   624  									Prefix: pointer.String(""),
   625  								},
   626  								Groups: apiserver.PrefixedClaimOrExpression{
   627  									Claim:  "groups",
   628  									Prefix: pointer.String("oidc:"),
   629  								},
   630  							},
   631  							ClaimValidationRules: []apiserver.ClaimValidationRule{
   632  								{
   633  									Claim:         "foo",
   634  									RequiredValue: "bar",
   635  								},
   636  							},
   637  						},
   638  					},
   639  				},
   640  				OIDCSigningAlgs: []string{"RS256"},
   641  			},
   642  		},
   643  	}
   644  
   645  	for _, testcase := range testCases {
   646  		t.Run(testcase.name, func(t *testing.T) {
   647  			opts := NewBuiltInAuthenticationOptions().WithOIDC()
   648  			pf := pflag.NewFlagSet("test-builtin-authentication-opts", pflag.ContinueOnError)
   649  			opts.AddFlags(pf)
   650  
   651  			if err := pf.Parse(testcase.args); err != nil {
   652  				t.Fatal(err)
   653  			}
   654  
   655  			resultConfig, err := opts.ToAuthenticationConfig()
   656  			if err != nil {
   657  				t.Fatal(err)
   658  			}
   659  			if !reflect.DeepEqual(resultConfig, testcase.expectConfig) {
   660  				t.Error(cmp.Diff(resultConfig, testcase.expectConfig))
   661  			}
   662  		})
   663  	}
   664  }
   665  
   666  func TestValidateOIDCOptions(t *testing.T) {
   667  	testCases := []struct {
   668  		name                                  string
   669  		args                                  []string
   670  		structuredAuthenticationConfigEnabled bool
   671  		expectErr                             string
   672  	}{
   673  		{
   674  			name: "issuer url and client id are not set",
   675  			args: []string{
   676  				"--oidc-username-claim=testClaim",
   677  			},
   678  			expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set",
   679  		},
   680  		{
   681  			name: "issuer url set, client id is not set",
   682  			args: []string{
   683  				"--oidc-issuer-url=https://testIssuerURL",
   684  				"--oidc-username-claim=testClaim",
   685  			},
   686  			expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set",
   687  		},
   688  		{
   689  			name: "issuer url is not set, client id is set",
   690  			args: []string{
   691  				"--oidc-client-id=testClientID",
   692  				"--oidc-username-claim=testClaim",
   693  			},
   694  			expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set",
   695  		},
   696  		{
   697  			name: "issuer url and client id are set",
   698  			args: []string{
   699  				"--oidc-client-id=testClientID",
   700  				"--oidc-issuer-url=https://testIssuerURL",
   701  			},
   702  			expectErr: "",
   703  		},
   704  		{
   705  			name: "authentication-config file, feature gate is not enabled",
   706  			args: []string{
   707  				"--authentication-config=configfile",
   708  			},
   709  			expectErr: "set --feature-gates=StructuredAuthenticationConfiguration=true to use authentication-config file",
   710  		},
   711  		{
   712  			name: "authentication-config file, --oidc-issuer-url is set",
   713  			args: []string{
   714  				"--authentication-config=configfile",
   715  				"--oidc-issuer-url=https://testIssuerURL",
   716  			},
   717  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   718  		},
   719  		{
   720  			name: "authentication-config file, --oidc-client-id is set",
   721  			args: []string{
   722  				"--authentication-config=configfile",
   723  				"--oidc-client-id=testClientID",
   724  			},
   725  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   726  		},
   727  		{
   728  			name: "authentication-config file, --oidc-username-claim is set",
   729  			args: []string{
   730  				"--authentication-config=configfile",
   731  				"--oidc-username-claim=testClaim",
   732  			},
   733  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   734  		},
   735  		{
   736  			name: "authentication-config file, --oidc-username-prefix is set",
   737  			args: []string{
   738  				"--authentication-config=configfile",
   739  				"--oidc-username-prefix=testPrefix",
   740  			},
   741  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   742  		},
   743  		{
   744  			name: "authentication-config file, --oidc-ca-file is set",
   745  			args: []string{
   746  				"--authentication-config=configfile",
   747  				"--oidc-ca-file=testCAFile",
   748  			},
   749  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   750  		},
   751  		{
   752  			name: "authentication-config file, --oidc-groups-claim is set",
   753  			args: []string{
   754  				"--authentication-config=configfile",
   755  				"--oidc-groups-claim=testClaim",
   756  			},
   757  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   758  		},
   759  		{
   760  			name: "authentication-config file, --oidc-groups-prefix is set",
   761  			args: []string{
   762  				"--authentication-config=configfile",
   763  				"--oidc-groups-prefix=testPrefix",
   764  			},
   765  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   766  		},
   767  		{
   768  			name: "authentication-config file, --oidc-required-claim is set",
   769  			args: []string{
   770  				"--authentication-config=configfile",
   771  				"--oidc-required-claim=foo=bar",
   772  			},
   773  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   774  		},
   775  		{
   776  			name: "authentication-config file, --oidc-signature-algs is set",
   777  			args: []string{
   778  				"--authentication-config=configfile",
   779  				"--oidc-signing-algs=RS512",
   780  			},
   781  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   782  		},
   783  		{
   784  			name: "authentication-config file, --oidc-username-claim flag not set, defaulting shouldn't error",
   785  			args: []string{
   786  				"--authentication-config=configfile",
   787  			},
   788  			expectErr:                             "",
   789  			structuredAuthenticationConfigEnabled: true,
   790  		},
   791  		{
   792  			name: "authentication-config file, --oidc-username-claim flag explicitly set with default value should error",
   793  			args: []string{
   794  				"--authentication-config=configfile",
   795  				"--oidc-username-claim=sub",
   796  			},
   797  			expectErr: "authentication-config file and oidc-* flags are mutually exclusive",
   798  		},
   799  		{
   800  			name: "valid authentication-config file",
   801  			args: []string{
   802  				"--authentication-config=configfile",
   803  			},
   804  			structuredAuthenticationConfigEnabled: true,
   805  			expectErr:                             "",
   806  		},
   807  	}
   808  
   809  	for _, tt := range testCases {
   810  		t.Run(tt.name, func(t *testing.T) {
   811  			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, tt.structuredAuthenticationConfigEnabled)()
   812  
   813  			opts := NewBuiltInAuthenticationOptions().WithOIDC()
   814  			pf := pflag.NewFlagSet("test-builtin-authentication-opts", pflag.ContinueOnError)
   815  			opts.AddFlags(pf)
   816  
   817  			if err := pf.Parse(tt.args); err != nil {
   818  				t.Fatal(err)
   819  			}
   820  
   821  			errs := opts.Validate()
   822  			if len(errs) > 0 && (!strings.Contains(utilerrors.NewAggregate(errs).Error(), tt.expectErr) || tt.expectErr == "") {
   823  				t.Errorf("Got err: %v, Expected err: %s", errs, tt.expectErr)
   824  			}
   825  			if len(errs) == 0 && len(tt.expectErr) != 0 {
   826  				t.Errorf("Got err nil, Expected err: %s", tt.expectErr)
   827  			}
   828  			if len(errs) > 0 && len(tt.expectErr) == 0 {
   829  				t.Errorf("Got err: %v, Expected err nil", errs)
   830  			}
   831  		})
   832  	}
   833  }
   834  
   835  func TestLoadAuthenticationConfig(t *testing.T) {
   836  	testCases := []struct {
   837  		name           string
   838  		file           func() string
   839  		expectErr      string
   840  		expectedConfig *apiserver.AuthenticationConfiguration
   841  	}{
   842  		{
   843  			name:           "empty file",
   844  			file:           func() string { return writeTempFile(t, ``) },
   845  			expectErr:      "empty config file",
   846  			expectedConfig: nil,
   847  		},
   848  		{
   849  			name: "valid file",
   850  			file: func() string {
   851  				return writeTempFile(t,
   852  					`{
   853  						"apiVersion":"apiserver.config.k8s.io/v1alpha1",
   854  						"kind":"AuthenticationConfiguration",
   855  						"jwt":[{"issuer":{"url": "https://test-issuer"}}]}`)
   856  			},
   857  			expectErr: "",
   858  			expectedConfig: &apiserver.AuthenticationConfiguration{
   859  				JWT: []apiserver.JWTAuthenticator{
   860  					{
   861  						Issuer: apiserver.Issuer{URL: "https://test-issuer"},
   862  					},
   863  				},
   864  			},
   865  		},
   866  		{
   867  			name:           "missing file",
   868  			file:           func() string { return "bogus-missing-file" },
   869  			expectErr:      "no such file or directory",
   870  			expectedConfig: nil,
   871  		},
   872  		{
   873  			name: "invalid content file",
   874  			file: func() string {
   875  				return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v99","kind":"AuthenticationConfiguration","authorizers":{"type":"Webhook"}}`)
   876  			},
   877  			expectErr:      `no kind "AuthenticationConfiguration" is registered for version "apiserver.config.k8s.io/v99"`,
   878  			expectedConfig: nil,
   879  		},
   880  		{
   881  			name:      "missing apiVersion",
   882  			file:      func() string { return writeTempFile(t, `{"kind":"AuthenticationConfiguration"}`) },
   883  			expectErr: `'apiVersion' is missing`,
   884  		},
   885  		{
   886  			name:      "missing kind",
   887  			file:      func() string { return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v1alpha1"}`) },
   888  			expectErr: `'Kind' is missing`,
   889  		},
   890  		{
   891  			name: "unknown group",
   892  			file: func() string {
   893  				return writeTempFile(t, `{"apiVersion":"apps/v1alpha1","kind":"AuthenticationConfiguration"}`)
   894  			},
   895  			expectErr: `apps/v1alpha1`,
   896  		},
   897  		{
   898  			name: "unknown version",
   899  			file: func() string {
   900  				return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v99","kind":"AuthenticationConfiguration"}`)
   901  			},
   902  			expectErr: `apiserver.config.k8s.io/v99`,
   903  		},
   904  		{
   905  			name: "unknown kind",
   906  			file: func() string {
   907  				return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v1alpha1","kind":"SomeConfiguration"}`)
   908  			},
   909  			expectErr: `SomeConfiguration`,
   910  		},
   911  		{
   912  			name: "unknown field",
   913  			file: func() string {
   914  				return writeTempFile(t, `{
   915  							"apiVersion":"apiserver.config.k8s.io/v1alpha1",
   916  							"kind":"AuthenticationConfiguration",
   917  							"jwt1":[{"issuer":{"url": "https://test-issuer"}}]}`)
   918  			},
   919  			expectErr: `unknown field "jwt1"`,
   920  		},
   921  		{
   922  			name: "v1alpha1 - json",
   923  			file: func() string {
   924  				return writeTempFile(t, `{
   925  							"apiVersion":"apiserver.config.k8s.io/v1alpha1",
   926  							"kind":"AuthenticationConfiguration",
   927  							"jwt":[{"issuer":{"url": "https://test-issuer"}}]}`)
   928  			},
   929  			expectedConfig: &apiserver.AuthenticationConfiguration{
   930  				JWT: []apiserver.JWTAuthenticator{
   931  					{
   932  						Issuer: apiserver.Issuer{
   933  							URL: "https://test-issuer",
   934  						},
   935  					},
   936  				},
   937  			},
   938  		},
   939  		{
   940  			name: "v1alpha1 - yaml",
   941  			file: func() string {
   942  				return writeTempFile(t, `
   943  apiVersion: apiserver.config.k8s.io/v1alpha1
   944  kind: AuthenticationConfiguration
   945  jwt:
   946  - issuer:
   947      url: https://test-issuer
   948    claimMappings:
   949      username:
   950        claim: sub
   951        prefix: ""
   952  `)
   953  			},
   954  			expectedConfig: &apiserver.AuthenticationConfiguration{
   955  				JWT: []apiserver.JWTAuthenticator{
   956  					{
   957  						Issuer: apiserver.Issuer{
   958  							URL: "https://test-issuer",
   959  						},
   960  						ClaimMappings: apiserver.ClaimMappings{
   961  							Username: apiserver.PrefixedClaimOrExpression{
   962  								Claim:  "sub",
   963  								Prefix: pointer.String(""),
   964  							},
   965  						},
   966  					},
   967  				},
   968  			},
   969  		},
   970  		{
   971  			name: "v1alpha1 - no jwt",
   972  			file: func() string {
   973  				return writeTempFile(t, `{
   974  							"apiVersion":"apiserver.config.k8s.io/v1alpha1",
   975  							"kind":"AuthenticationConfiguration"}`)
   976  			},
   977  			expectedConfig: &apiserver.AuthenticationConfiguration{},
   978  		},
   979  	}
   980  
   981  	for _, tc := range testCases {
   982  		t.Run(tc.name, func(t *testing.T) {
   983  			config, err := loadAuthenticationConfig(tc.file())
   984  			if !strings.Contains(errString(err), tc.expectErr) {
   985  				t.Fatalf("expected error %q, got %v", tc.expectErr, err)
   986  			}
   987  			if !reflect.DeepEqual(config, tc.expectedConfig) {
   988  				t.Fatalf("unexpected config:\n%s", cmp.Diff(tc.expectedConfig, config))
   989  			}
   990  		})
   991  	}
   992  }
   993  
   994  func writeTempFile(t *testing.T, content string) string {
   995  	t.Helper()
   996  	file, err := os.CreateTemp("", "config")
   997  	if err != nil {
   998  		t.Fatal(err)
   999  	}
  1000  	t.Cleanup(func() {
  1001  		if err := os.Remove(file.Name()); err != nil {
  1002  			t.Fatal(err)
  1003  		}
  1004  	})
  1005  	if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
  1006  		t.Fatal(err)
  1007  	}
  1008  	return file.Name()
  1009  }
  1010  
  1011  func errString(err error) string {
  1012  	if err == nil {
  1013  		return ""
  1014  	}
  1015  	return err.Error()
  1016  }