k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/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 "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 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, f, true) 250 } 251 for _, f := range testcase.disabledFeatures { 252 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 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 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 }