1
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,
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,
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
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
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
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