...

Source file src/k8s.io/kubernetes/pkg/kubeapiserver/options/authentication_test.go

Documentation: k8s.io/kubernetes/pkg/kubeapiserver/options

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

View as plain text