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 }