k8s.io/apiserver@v0.31.1/pkg/apis/apiserver/validation/validation_test.go (about) 1 /* 2 Copyright 2023 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 validation 18 19 import ( 20 "crypto/ecdsa" 21 "crypto/elliptic" 22 "crypto/rand" 23 "encoding/pem" 24 "fmt" 25 "os" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/util/errors" 33 "k8s.io/apimachinery/pkg/util/sets" 34 "k8s.io/apimachinery/pkg/util/validation/field" 35 api "k8s.io/apiserver/pkg/apis/apiserver" 36 authenticationcel "k8s.io/apiserver/pkg/authentication/cel" 37 "k8s.io/apiserver/pkg/cel/environment" 38 "k8s.io/apiserver/pkg/features" 39 utilfeature "k8s.io/apiserver/pkg/util/feature" 40 certutil "k8s.io/client-go/util/cert" 41 featuregatetesting "k8s.io/component-base/featuregate/testing" 42 "k8s.io/utils/pointer" 43 ) 44 45 var ( 46 compiler = authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) 47 ) 48 49 func TestValidateAuthenticationConfiguration(t *testing.T) { 50 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true) 51 52 testCases := []struct { 53 name string 54 in *api.AuthenticationConfiguration 55 disallowedIssuers []string 56 want string 57 }{ 58 { 59 name: "jwt authenticator is empty", 60 in: &api.AuthenticationConfiguration{}, 61 want: "", 62 }, 63 { 64 name: "duplicate issuer across jwt authenticators", 65 in: &api.AuthenticationConfiguration{ 66 JWT: []api.JWTAuthenticator{ 67 { 68 Issuer: api.Issuer{ 69 URL: "https://issuer-url", 70 Audiences: []string{"audience"}, 71 }, 72 ClaimValidationRules: []api.ClaimValidationRule{ 73 { 74 Claim: "foo", 75 RequiredValue: "bar", 76 }, 77 }, 78 ClaimMappings: api.ClaimMappings{ 79 Username: api.PrefixedClaimOrExpression{ 80 Claim: "sub", 81 Prefix: pointer.String("prefix"), 82 }, 83 }, 84 }, 85 { 86 Issuer: api.Issuer{ 87 URL: "https://issuer-url", 88 Audiences: []string{"audience"}, 89 }, 90 ClaimValidationRules: []api.ClaimValidationRule{ 91 { 92 Claim: "foo", 93 RequiredValue: "bar", 94 }, 95 }, 96 ClaimMappings: api.ClaimMappings{ 97 Username: api.PrefixedClaimOrExpression{ 98 Claim: "sub", 99 Prefix: pointer.String("prefix"), 100 }, 101 }, 102 }, 103 }, 104 }, 105 want: `jwt[1].issuer.url: Duplicate value: "https://issuer-url"`, 106 }, 107 { 108 name: "duplicate discoveryURL across jwt authenticators", 109 in: &api.AuthenticationConfiguration{ 110 JWT: []api.JWTAuthenticator{ 111 { 112 Issuer: api.Issuer{ 113 URL: "https://issuer-url", 114 DiscoveryURL: "https://discovery-url/.well-known/openid-configuration", 115 Audiences: []string{"audience"}, 116 }, 117 ClaimValidationRules: []api.ClaimValidationRule{ 118 { 119 Claim: "foo", 120 RequiredValue: "bar", 121 }, 122 }, 123 ClaimMappings: api.ClaimMappings{ 124 Username: api.PrefixedClaimOrExpression{ 125 Claim: "sub", 126 Prefix: pointer.String("prefix"), 127 }, 128 }, 129 }, 130 { 131 Issuer: api.Issuer{ 132 URL: "https://different-issuer-url", 133 DiscoveryURL: "https://discovery-url/.well-known/openid-configuration", 134 Audiences: []string{"audience"}, 135 }, 136 ClaimValidationRules: []api.ClaimValidationRule{ 137 { 138 Claim: "foo", 139 RequiredValue: "bar", 140 }, 141 }, 142 ClaimMappings: api.ClaimMappings{ 143 Username: api.PrefixedClaimOrExpression{ 144 Claim: "sub", 145 Prefix: pointer.String("prefix"), 146 }, 147 }, 148 }, 149 }, 150 }, 151 want: `jwt[1].issuer.discoveryURL: Duplicate value: "https://discovery-url/.well-known/openid-configuration"`, 152 }, 153 { 154 name: "failed issuer validation", 155 in: &api.AuthenticationConfiguration{ 156 JWT: []api.JWTAuthenticator{ 157 { 158 Issuer: api.Issuer{ 159 URL: "invalid-url", 160 Audiences: []string{"audience"}, 161 }, 162 ClaimMappings: api.ClaimMappings{ 163 Username: api.PrefixedClaimOrExpression{ 164 Claim: "claim", 165 Prefix: pointer.String("prefix"), 166 }, 167 }, 168 }, 169 }, 170 }, 171 want: `jwt[0].issuer.url: Invalid value: "invalid-url": URL scheme must be https`, 172 }, 173 { 174 name: "failed claimValidationRule validation", 175 in: &api.AuthenticationConfiguration{ 176 JWT: []api.JWTAuthenticator{ 177 { 178 Issuer: api.Issuer{ 179 URL: "https://issuer-url", 180 Audiences: []string{"audience"}, 181 }, 182 ClaimValidationRules: []api.ClaimValidationRule{ 183 { 184 Claim: "foo", 185 RequiredValue: "bar", 186 }, 187 { 188 Claim: "foo", 189 RequiredValue: "baz", 190 }, 191 }, 192 ClaimMappings: api.ClaimMappings{ 193 Username: api.PrefixedClaimOrExpression{ 194 Claim: "claim", 195 Prefix: pointer.String("prefix"), 196 }, 197 }, 198 }, 199 }, 200 }, 201 want: `jwt[0].claimValidationRules[1].claim: Duplicate value: "foo"`, 202 }, 203 { 204 name: "failed claimMapping validation", 205 in: &api.AuthenticationConfiguration{ 206 JWT: []api.JWTAuthenticator{ 207 { 208 Issuer: api.Issuer{ 209 URL: "https://issuer-url", 210 Audiences: []string{"audience"}, 211 }, 212 ClaimValidationRules: []api.ClaimValidationRule{ 213 { 214 Claim: "foo", 215 RequiredValue: "bar", 216 }, 217 }, 218 ClaimMappings: api.ClaimMappings{ 219 Username: api.PrefixedClaimOrExpression{ 220 Prefix: pointer.String("prefix"), 221 }, 222 }, 223 }, 224 }, 225 }, 226 want: "jwt[0].claimMappings.username: Required value: claim or expression is required", 227 }, 228 { 229 name: "failed userValidationRule validation", 230 in: &api.AuthenticationConfiguration{ 231 JWT: []api.JWTAuthenticator{ 232 { 233 Issuer: api.Issuer{ 234 URL: "https://issuer-url", 235 Audiences: []string{"audience"}, 236 }, 237 ClaimValidationRules: []api.ClaimValidationRule{ 238 { 239 Claim: "foo", 240 RequiredValue: "bar", 241 }, 242 }, 243 ClaimMappings: api.ClaimMappings{ 244 Username: api.PrefixedClaimOrExpression{ 245 Claim: "sub", 246 Prefix: pointer.String("prefix"), 247 }, 248 }, 249 UserValidationRules: []api.UserValidationRule{ 250 {Expression: "user.username == 'foo'"}, 251 {Expression: "user.username == 'foo'"}, 252 }, 253 }, 254 }, 255 }, 256 want: `jwt[0].userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`, 257 }, 258 { 259 name: "valid authentication configuration with disallowed issuer", 260 in: &api.AuthenticationConfiguration{ 261 JWT: []api.JWTAuthenticator{ 262 { 263 Issuer: api.Issuer{ 264 URL: "https://issuer-url", 265 Audiences: []string{"audience"}, 266 }, 267 ClaimValidationRules: []api.ClaimValidationRule{ 268 { 269 Claim: "foo", 270 RequiredValue: "bar", 271 }, 272 }, 273 ClaimMappings: api.ClaimMappings{ 274 Username: api.PrefixedClaimOrExpression{ 275 Claim: "sub", 276 Prefix: pointer.String("prefix"), 277 }, 278 }, 279 }, 280 }, 281 }, 282 disallowedIssuers: []string{"a", "b", "https://issuer-url", "c"}, 283 want: `jwt[0].issuer.url: Invalid value: "https://issuer-url": URL must not overlap with disallowed issuers: [a b c https://issuer-url]`, 284 }, 285 { 286 name: "valid authentication configuration that uses unverified email", 287 in: &api.AuthenticationConfiguration{ 288 JWT: []api.JWTAuthenticator{ 289 { 290 Issuer: api.Issuer{ 291 URL: "https://issuer-url", 292 Audiences: []string{"audience"}, 293 }, 294 ClaimValidationRules: []api.ClaimValidationRule{ 295 { 296 Claim: "foo", 297 RequiredValue: "bar", 298 }, 299 }, 300 ClaimMappings: api.ClaimMappings{ 301 Username: api.PrefixedClaimOrExpression{ 302 Expression: "claims.email", 303 }, 304 }, 305 }, 306 }, 307 }, 308 want: `jwt[0].claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 309 }, 310 { 311 name: "valid authentication configuration that almost uses unverified email", 312 in: &api.AuthenticationConfiguration{ 313 JWT: []api.JWTAuthenticator{ 314 { 315 Issuer: api.Issuer{ 316 URL: "https://issuer-url", 317 Audiences: []string{"audience"}, 318 }, 319 ClaimValidationRules: []api.ClaimValidationRule{ 320 { 321 Claim: "foo", 322 RequiredValue: "bar", 323 }, 324 }, 325 ClaimMappings: api.ClaimMappings{ 326 Username: api.PrefixedClaimOrExpression{ 327 Expression: "claims.email_", 328 }, 329 }, 330 }, 331 }, 332 }, 333 want: "", 334 }, 335 { 336 name: "valid authentication configuration that uses unverified email join", 337 in: &api.AuthenticationConfiguration{ 338 JWT: []api.JWTAuthenticator{ 339 { 340 Issuer: api.Issuer{ 341 URL: "https://issuer-url", 342 Audiences: []string{"audience"}, 343 }, 344 ClaimValidationRules: []api.ClaimValidationRule{ 345 { 346 Claim: "foo", 347 RequiredValue: "bar", 348 }, 349 }, 350 ClaimMappings: api.ClaimMappings{ 351 Username: api.PrefixedClaimOrExpression{ 352 Expression: `['yay', string(claims.email), 'panda'].join(' ')`, 353 }, 354 }, 355 }, 356 }, 357 }, 358 want: `jwt[0].claimMappings.username.expression: Invalid value: "['yay', string(claims.email), 'panda'].join(' ')": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 359 }, 360 { 361 name: "valid authentication configuration that uses unverified optional email", 362 in: &api.AuthenticationConfiguration{ 363 JWT: []api.JWTAuthenticator{ 364 { 365 Issuer: api.Issuer{ 366 URL: "https://issuer-url", 367 Audiences: []string{"audience"}, 368 }, 369 ClaimValidationRules: []api.ClaimValidationRule{ 370 { 371 Claim: "foo", 372 RequiredValue: "bar", 373 }, 374 }, 375 ClaimMappings: api.ClaimMappings{ 376 Username: api.PrefixedClaimOrExpression{ 377 Expression: `claims.?email`, 378 }, 379 }, 380 }, 381 }, 382 }, 383 want: `jwt[0].claimMappings.username.expression: Invalid value: "claims.?email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 384 }, 385 { 386 name: "valid authentication configuration that uses unverified optional map email key", 387 in: &api.AuthenticationConfiguration{ 388 JWT: []api.JWTAuthenticator{ 389 { 390 Issuer: api.Issuer{ 391 URL: "https://issuer-url", 392 Audiences: []string{"audience"}, 393 }, 394 ClaimValidationRules: []api.ClaimValidationRule{ 395 { 396 Claim: "foo", 397 RequiredValue: "bar", 398 }, 399 }, 400 ClaimMappings: api.ClaimMappings{ 401 Username: api.PrefixedClaimOrExpression{ 402 Expression: `{claims.?email: "panda"}`, 403 }, 404 }, 405 }, 406 }, 407 }, 408 want: `jwt[0].claimMappings.username.expression: Invalid value: "{claims.?email: \"panda\"}": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 409 }, 410 { 411 name: "valid authentication configuration that uses unverified optional map email value", 412 in: &api.AuthenticationConfiguration{ 413 JWT: []api.JWTAuthenticator{ 414 { 415 Issuer: api.Issuer{ 416 URL: "https://issuer-url", 417 Audiences: []string{"audience"}, 418 }, 419 ClaimValidationRules: []api.ClaimValidationRule{ 420 { 421 Claim: "foo", 422 RequiredValue: "bar", 423 }, 424 }, 425 ClaimMappings: api.ClaimMappings{ 426 Username: api.PrefixedClaimOrExpression{ 427 Expression: `{"fancy": claims.?email}`, 428 }, 429 }, 430 }, 431 }, 432 }, 433 want: `jwt[0].claimMappings.username.expression: Invalid value: "{\"fancy\": claims.?email}": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 434 }, 435 { 436 name: "valid authentication configuration that uses unverified email value in list iteration", 437 in: &api.AuthenticationConfiguration{ 438 JWT: []api.JWTAuthenticator{ 439 { 440 Issuer: api.Issuer{ 441 URL: "https://issuer-url", 442 Audiences: []string{"audience"}, 443 }, 444 ClaimValidationRules: []api.ClaimValidationRule{ 445 { 446 Claim: "foo", 447 RequiredValue: "bar", 448 }, 449 }, 450 ClaimMappings: api.ClaimMappings{ 451 Username: api.PrefixedClaimOrExpression{ 452 Expression: `["a"].map(i, i + claims.email)`, 453 }, 454 }, 455 }, 456 }, 457 }, 458 want: `jwt[0].claimMappings.username.expression: Invalid value: "[\"a\"].map(i, i + claims.email)": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 459 }, 460 { 461 name: "valid authentication configuration that uses verified email join via rule", 462 in: &api.AuthenticationConfiguration{ 463 JWT: []api.JWTAuthenticator{ 464 { 465 Issuer: api.Issuer{ 466 URL: "https://issuer-url", 467 Audiences: []string{"audience"}, 468 }, 469 ClaimValidationRules: []api.ClaimValidationRule{ 470 { 471 Expression: `string(claims.email_verified) == "panda"`, 472 }, 473 }, 474 ClaimMappings: api.ClaimMappings{ 475 Username: api.PrefixedClaimOrExpression{ 476 Expression: `['yay', string(claims.email), 'panda'].join(' ')`, 477 }, 478 }, 479 }, 480 }, 481 }, 482 want: "", 483 }, 484 { 485 name: "valid authentication configuration that uses verified email join via extra", 486 in: &api.AuthenticationConfiguration{ 487 JWT: []api.JWTAuthenticator{ 488 { 489 Issuer: api.Issuer{ 490 URL: "https://issuer-url", 491 Audiences: []string{"audience"}, 492 }, 493 ClaimValidationRules: []api.ClaimValidationRule{ 494 { 495 Claim: "foo", 496 RequiredValue: "bar", 497 }, 498 }, 499 ClaimMappings: api.ClaimMappings{ 500 Username: api.PrefixedClaimOrExpression{ 501 Expression: `['yay', string(claims.email), 'panda'].join(' ')`, 502 }, 503 Extra: []api.ExtraMapping{ 504 {Key: "panda.io/foo", ValueExpression: "claims.email_verified.upperAscii()"}, 505 }, 506 }, 507 }, 508 }, 509 }, 510 want: "", 511 }, 512 { 513 name: "valid authentication configuration that uses verified email join via extra optional", 514 in: &api.AuthenticationConfiguration{ 515 JWT: []api.JWTAuthenticator{ 516 { 517 Issuer: api.Issuer{ 518 URL: "https://issuer-url", 519 Audiences: []string{"audience"}, 520 }, 521 ClaimValidationRules: []api.ClaimValidationRule{ 522 { 523 Claim: "foo", 524 RequiredValue: "bar", 525 }, 526 }, 527 ClaimMappings: api.ClaimMappings{ 528 Username: api.PrefixedClaimOrExpression{ 529 Expression: `['yay', string(claims.email), 'panda'].join(' ')`, 530 }, 531 Extra: []api.ExtraMapping{ 532 {Key: "panda.io/foo", ValueExpression: "claims.?email_verified"}, 533 }, 534 }, 535 }, 536 }, 537 }, 538 want: "", 539 }, 540 { 541 name: "valid authentication configuration that uses email and email_verified || true via username", 542 in: &api.AuthenticationConfiguration{ 543 JWT: []api.JWTAuthenticator{ 544 { 545 Issuer: api.Issuer{ 546 URL: "https://issuer-url", 547 Audiences: []string{"audience"}, 548 }, 549 ClaimValidationRules: []api.ClaimValidationRule{ 550 { 551 Claim: "foo", 552 RequiredValue: "bar", 553 }, 554 }, 555 // allow email claim when email_verified is true or absent 556 ClaimMappings: api.ClaimMappings{ 557 Username: api.PrefixedClaimOrExpression{ 558 Expression: `claims.?email_verified.orValue(true) ? claims.email : claims.sub`, 559 }, 560 }, 561 }, 562 }, 563 }, 564 want: "", 565 }, 566 { 567 name: "valid authentication configuration that uses email and email_verified || false via username", 568 in: &api.AuthenticationConfiguration{ 569 JWT: []api.JWTAuthenticator{ 570 { 571 Issuer: api.Issuer{ 572 URL: "https://issuer-url", 573 Audiences: []string{"audience"}, 574 }, 575 ClaimValidationRules: []api.ClaimValidationRule{ 576 { 577 Claim: "foo", 578 RequiredValue: "bar", 579 }, 580 }, 581 // allow email claim only when email_verified is present and true 582 ClaimMappings: api.ClaimMappings{ 583 Username: api.PrefixedClaimOrExpression{ 584 Expression: `claims.?email_verified.orValue(false) ? claims.email : claims.sub`, 585 }, 586 }, 587 }, 588 }, 589 }, 590 want: "", 591 }, 592 { 593 name: "valid authentication configuration", 594 in: &api.AuthenticationConfiguration{ 595 JWT: []api.JWTAuthenticator{ 596 { 597 Issuer: api.Issuer{ 598 URL: "https://issuer-url", 599 Audiences: []string{"audience"}, 600 }, 601 ClaimValidationRules: []api.ClaimValidationRule{ 602 { 603 Claim: "foo", 604 RequiredValue: "bar", 605 }, 606 }, 607 ClaimMappings: api.ClaimMappings{ 608 Username: api.PrefixedClaimOrExpression{ 609 Claim: "sub", 610 Prefix: pointer.String("prefix"), 611 }, 612 }, 613 }, 614 }, 615 }, 616 want: "", 617 }, 618 } 619 620 for _, tt := range testCases { 621 t.Run(tt.name, func(t *testing.T) { 622 got := ValidateAuthenticationConfiguration(tt.in, tt.disallowedIssuers).ToAggregate() 623 if d := cmp.Diff(tt.want, errString(got)); d != "" { 624 t.Fatalf("AuthenticationConfiguration validation mismatch (-want +got):\n%s", d) 625 } 626 }) 627 } 628 } 629 630 func TestValidateIssuerURL(t *testing.T) { 631 fldPath := field.NewPath("issuer", "url") 632 633 testCases := []struct { 634 name string 635 in string 636 disallowedIssuers sets.Set[string] 637 want string 638 }{ 639 { 640 name: "url is empty", 641 in: "", 642 want: "issuer.url: Required value: URL is required", 643 }, 644 { 645 name: "url parse error", 646 in: "https://issuer-url:invalid-port", 647 want: `issuer.url: Invalid value: "https://issuer-url:invalid-port": parse "https://issuer-url:invalid-port": invalid port ":invalid-port" after host`, 648 }, 649 { 650 name: "url is not https", 651 in: "http://issuer-url", 652 want: `issuer.url: Invalid value: "http://issuer-url": URL scheme must be https`, 653 }, 654 { 655 name: "url user info is not allowed", 656 in: "https://user:pass@issuer-url", 657 want: `issuer.url: Invalid value: "https://user:pass@issuer-url": URL must not contain a username or password`, 658 }, 659 { 660 name: "url raw query is not allowed", 661 in: "https://issuer-url?query", 662 want: `issuer.url: Invalid value: "https://issuer-url?query": URL must not contain a query`, 663 }, 664 { 665 name: "url fragment is not allowed", 666 in: "https://issuer-url#fragment", 667 want: `issuer.url: Invalid value: "https://issuer-url#fragment": URL must not contain a fragment`, 668 }, 669 { 670 name: "valid url that is disallowed", 671 in: "https://issuer-url", 672 disallowedIssuers: sets.New("https://issuer-url"), 673 want: `issuer.url: Invalid value: "https://issuer-url": URL must not overlap with disallowed issuers: [https://issuer-url]`, 674 }, 675 { 676 name: "valid url", 677 in: "https://issuer-url", 678 want: "", 679 }, 680 } 681 682 for _, tt := range testCases { 683 t.Run(tt.name, func(t *testing.T) { 684 got := validateIssuerURL(tt.in, tt.disallowedIssuers, fldPath).ToAggregate() 685 if d := cmp.Diff(tt.want, errString(got)); d != "" { 686 t.Fatalf("URL validation mismatch (-want +got):\n%s", d) 687 } 688 }) 689 } 690 } 691 692 func TestValidateIssuerDiscoveryURL(t *testing.T) { 693 fldPath := field.NewPath("issuer", "discoveryURL") 694 695 testCases := []struct { 696 name string 697 in string 698 issuerURL string 699 want string 700 }{ 701 { 702 name: "url is empty", 703 in: "", 704 want: "", 705 }, 706 { 707 name: "url parse error", 708 in: "https://oidc.oidc-namespace.svc:invalid-port", 709 want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc:invalid-port": parse "https://oidc.oidc-namespace.svc:invalid-port": invalid port ":invalid-port" after host`, 710 }, 711 { 712 name: "url is not https", 713 in: "http://oidc.oidc-namespace.svc", 714 want: `issuer.discoveryURL: Invalid value: "http://oidc.oidc-namespace.svc": URL scheme must be https`, 715 }, 716 { 717 name: "url user info is not allowed", 718 in: "https://user:pass@oidc.oidc-namespace.svc", 719 want: `issuer.discoveryURL: Invalid value: "https://user:pass@oidc.oidc-namespace.svc": URL must not contain a username or password`, 720 }, 721 { 722 name: "url raw query is not allowed", 723 in: "https://oidc.oidc-namespace.svc?query", 724 want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc?query": URL must not contain a query`, 725 }, 726 { 727 name: "url fragment is not allowed", 728 in: "https://oidc.oidc-namespace.svc#fragment", 729 want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc#fragment": URL must not contain a fragment`, 730 }, 731 { 732 name: "valid url", 733 in: "https://oidc.oidc-namespace.svc", 734 want: "", 735 }, 736 { 737 name: "valid url with path", 738 in: "https://oidc.oidc-namespace.svc/path", 739 want: "", 740 }, 741 { 742 name: "discovery url same as issuer url", 743 issuerURL: "https://issuer-url", 744 in: "https://issuer-url", 745 want: `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`, 746 }, 747 { 748 name: "discovery url same as issuer url, with trailing slash", 749 issuerURL: "https://issuer-url", 750 in: "https://issuer-url/", 751 want: `issuer.discoveryURL: Invalid value: "https://issuer-url/": discoveryURL must be different from URL`, 752 }, 753 { 754 name: "discovery url same as issuer url, with multiple trailing slashes", 755 issuerURL: "https://issuer-url", 756 in: "https://issuer-url///", 757 want: `issuer.discoveryURL: Invalid value: "https://issuer-url///": discoveryURL must be different from URL`, 758 }, 759 { 760 name: "discovery url same as issuer url, issuer url with trailing slash", 761 issuerURL: "https://issuer-url/", 762 in: "https://issuer-url", 763 want: `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`, 764 }, 765 } 766 767 for _, tt := range testCases { 768 t.Run(tt.name, func(t *testing.T) { 769 got := validateIssuerDiscoveryURL(tt.issuerURL, tt.in, fldPath).ToAggregate() 770 if d := cmp.Diff(tt.want, errString(got)); d != "" { 771 t.Fatalf("URL validation mismatch (-want +got):\n%s", d) 772 } 773 }) 774 } 775 } 776 777 func TestValidateAudiences(t *testing.T) { 778 fldPath := field.NewPath("issuer", "audiences") 779 audienceMatchPolicyFldPath := field.NewPath("issuer", "audienceMatchPolicy") 780 781 testCases := []struct { 782 name string 783 in []string 784 matchPolicy string 785 want string 786 }{ 787 { 788 name: "audiences is empty", 789 in: []string{}, 790 want: "issuer.audiences: Required value: at least one issuer.audiences is required", 791 }, 792 { 793 name: "audience is empty", 794 in: []string{""}, 795 want: "issuer.audiences[0]: Required value: audience can't be empty", 796 }, 797 { 798 name: "invalid match policy with single audience", 799 in: []string{"audience"}, 800 matchPolicy: "MatchExact", 801 want: `issuer.audienceMatchPolicy: Invalid value: "MatchExact": audienceMatchPolicy must be empty or MatchAny for single audience`, 802 }, 803 { 804 name: "valid audience", 805 in: []string{"audience"}, 806 want: "", 807 }, 808 { 809 name: "valid audience with MatchAny policy", 810 in: []string{"audience"}, 811 matchPolicy: "MatchAny", 812 want: "", 813 }, 814 { 815 name: "duplicate audience", 816 in: []string{"audience", "audience"}, 817 matchPolicy: "MatchAny", 818 want: `issuer.audiences[1]: Duplicate value: "audience"`, 819 }, 820 { 821 name: "match policy not set with multiple audiences", 822 in: []string{"audience1", "audience2"}, 823 want: `issuer.audienceMatchPolicy: Invalid value: "": audienceMatchPolicy must be MatchAny for multiple audiences`, 824 }, 825 { 826 name: "valid multiple audiences", 827 in: []string{"audience1", "audience2"}, 828 matchPolicy: "MatchAny", 829 want: "", 830 }, 831 } 832 833 for _, tt := range testCases { 834 t.Run(tt.name, func(t *testing.T) { 835 got := validateAudiences(tt.in, api.AudienceMatchPolicyType(tt.matchPolicy), fldPath, audienceMatchPolicyFldPath).ToAggregate() 836 if d := cmp.Diff(tt.want, errString(got)); d != "" { 837 t.Fatalf("Audiences validation mismatch (-want +got):\n%s", d) 838 } 839 }) 840 } 841 } 842 843 func TestValidateCertificateAuthority(t *testing.T) { 844 fldPath := field.NewPath("issuer", "certificateAuthority") 845 846 testCases := []struct { 847 name string 848 in func() string 849 want string 850 }{ 851 { 852 name: "invalid certificate authority", 853 in: func() string { return "invalid" }, 854 want: `issuer.certificateAuthority: Invalid value: "<omitted>": data does not contain any valid RSA or ECDSA certificates`, 855 }, 856 { 857 name: "certificate authority is empty", 858 in: func() string { return "" }, 859 want: "", 860 }, 861 { 862 name: "valid certificate authority", 863 in: func() string { 864 caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 865 if err != nil { 866 t.Fatal(err) 867 } 868 caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey) 869 if err != nil { 870 t.Fatal(err) 871 } 872 return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})) 873 }, 874 want: "", 875 }, 876 } 877 878 for _, tt := range testCases { 879 t.Run(tt.name, func(t *testing.T) { 880 got := validateCertificateAuthority(tt.in(), fldPath).ToAggregate() 881 if d := cmp.Diff(tt.want, errString(got)); d != "" { 882 t.Fatalf("CertificateAuthority validation mismatch (-want +got):\n%s", d) 883 } 884 }) 885 } 886 } 887 888 func TestValidateClaimValidationRules(t *testing.T) { 889 fldPath := field.NewPath("issuer", "claimValidationRules") 890 891 testCases := []struct { 892 name string 893 in []api.ClaimValidationRule 894 structuredAuthnFeatureEnabled bool 895 want string 896 wantCELMapper bool 897 wantUsesEmailVerifiedClaim bool 898 }{ 899 { 900 name: "claim and expression are empty, structured authn feature enabled", 901 in: []api.ClaimValidationRule{{}}, 902 structuredAuthnFeatureEnabled: true, 903 want: "issuer.claimValidationRules[0]: Required value: claim or expression is required", 904 }, 905 { 906 name: "claim and expression are set", 907 in: []api.ClaimValidationRule{ 908 {Claim: "claim", Expression: "expression"}, 909 }, 910 structuredAuthnFeatureEnabled: true, 911 want: `issuer.claimValidationRules[0]: Invalid value: "claim": claim and expression can't both be set`, 912 }, 913 { 914 name: "message set when claim is set", 915 in: []api.ClaimValidationRule{ 916 {Claim: "claim", Message: "message"}, 917 }, 918 structuredAuthnFeatureEnabled: true, 919 want: `issuer.claimValidationRules[0].message: Invalid value: "message": message can't be set when claim is set`, 920 }, 921 { 922 name: "requiredValue set when expression is set", 923 in: []api.ClaimValidationRule{ 924 {Expression: "claims.foo == 'bar'", RequiredValue: "value"}, 925 }, 926 structuredAuthnFeatureEnabled: true, 927 want: `issuer.claimValidationRules[0].requiredValue: Invalid value: "value": requiredValue can't be set when expression is set`, 928 }, 929 { 930 name: "duplicate claim", 931 in: []api.ClaimValidationRule{ 932 {Claim: "claim"}, 933 {Claim: "claim"}, 934 }, 935 structuredAuthnFeatureEnabled: true, 936 want: `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`, 937 }, 938 { 939 name: "duplicate expression", 940 in: []api.ClaimValidationRule{ 941 {Expression: "claims.foo == 'bar'"}, 942 {Expression: "claims.foo == 'bar'"}, 943 }, 944 structuredAuthnFeatureEnabled: true, 945 want: `issuer.claimValidationRules[1].expression: Duplicate value: "claims.foo == 'bar'"`, 946 }, 947 { 948 name: "expression set when structured authn feature is disabled", 949 in: []api.ClaimValidationRule{ 950 {Expression: "claims.foo == 'bar'"}, 951 }, 952 structuredAuthnFeatureEnabled: false, 953 want: `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo == 'bar'": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled`, 954 }, 955 { 956 name: "CEL expression compilation error", 957 in: []api.ClaimValidationRule{ 958 {Expression: "foo.bar"}, 959 }, 960 structuredAuthnFeatureEnabled: true, 961 want: `issuer.claimValidationRules[0].expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 962 | foo.bar 963 | ^`, 964 }, 965 { 966 name: "expression does not evaluate to bool", 967 in: []api.ClaimValidationRule{ 968 {Expression: "claims.foo"}, 969 }, 970 structuredAuthnFeatureEnabled: true, 971 want: `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo": must evaluate to bool`, 972 }, 973 { 974 name: "valid claim validation rule with expression", 975 in: []api.ClaimValidationRule{ 976 {Expression: "claims.foo == 'bar'"}, 977 }, 978 structuredAuthnFeatureEnabled: true, 979 want: "", 980 wantCELMapper: true, 981 }, 982 { 983 name: "valid claim validation rule with multiple rules and email_verified check", 984 in: []api.ClaimValidationRule{ 985 {Claim: "claim1", RequiredValue: "value1"}, 986 {Claim: "claim2", RequiredValue: "value2"}, 987 {Expression: "has(claims.email_verified)"}, 988 }, 989 structuredAuthnFeatureEnabled: true, 990 want: "", 991 wantUsesEmailVerifiedClaim: true, 992 }, 993 { 994 name: "valid claim validation rule with multiple rules and almost email_verified check", 995 in: []api.ClaimValidationRule{ 996 {Claim: "claim1", RequiredValue: "value1"}, 997 {Claim: "claim2", RequiredValue: "value2"}, 998 {Expression: "has(claims.email_verified_)"}, 999 }, 1000 structuredAuthnFeatureEnabled: true, 1001 want: "", 1002 wantUsesEmailVerifiedClaim: false, 1003 }, 1004 { 1005 name: "valid claim validation rule with multiple rules", 1006 in: []api.ClaimValidationRule{ 1007 {Claim: "claim1", RequiredValue: "value1"}, 1008 {Claim: "claim2", RequiredValue: "claims.email_verified"}, // not a CEL expression 1009 }, 1010 structuredAuthnFeatureEnabled: true, 1011 want: "", 1012 wantUsesEmailVerifiedClaim: false, 1013 }, 1014 } 1015 1016 for _, tt := range testCases { 1017 t.Run(tt.name, func(t *testing.T) { 1018 state := &validationState{} 1019 got := validateClaimValidationRules(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate() 1020 if d := cmp.Diff(tt.want, errString(got)); d != "" { 1021 t.Fatalf("ClaimValidationRules validation mismatch (-want +got):\n%s", d) 1022 } 1023 if tt.wantCELMapper && state.mapper.ClaimValidationRules == nil { 1024 t.Fatalf("ClaimValidationRules validation mismatch: CELMapper.ClaimValidationRules is nil") 1025 } 1026 if tt.wantUsesEmailVerifiedClaim != state.usesEmailVerifiedClaim { 1027 t.Fatalf("ClaimValidationRules state.usesEmailVerifiedClaim mismatch: want %v, got %v", tt.wantUsesEmailVerifiedClaim, state.usesEmailVerifiedClaim) 1028 } 1029 }) 1030 } 1031 } 1032 1033 func TestValidateClaimMappings(t *testing.T) { 1034 fldPath := field.NewPath("issuer", "claimMappings") 1035 1036 testCases := []struct { 1037 name string 1038 in api.ClaimMappings 1039 usesEmailVerifiedClaim bool 1040 structuredAuthnFeatureEnabled bool 1041 want string 1042 wantCELMapper bool 1043 }{ 1044 { 1045 name: "username expression and claim are set", 1046 in: api.ClaimMappings{ 1047 Username: api.PrefixedClaimOrExpression{ 1048 Claim: "claim", 1049 Expression: "claims.username", 1050 }, 1051 }, 1052 structuredAuthnFeatureEnabled: true, 1053 want: `issuer.claimMappings.username: Invalid value: "": claim and expression can't both be set`, 1054 }, 1055 { 1056 name: "username expression and claim are empty", 1057 in: api.ClaimMappings{Username: api.PrefixedClaimOrExpression{}}, 1058 structuredAuthnFeatureEnabled: true, 1059 want: "issuer.claimMappings.username: Required value: claim or expression is required", 1060 }, 1061 { 1062 name: "username prefix set when expression is set", 1063 in: api.ClaimMappings{ 1064 Username: api.PrefixedClaimOrExpression{ 1065 Expression: "claims.username", 1066 Prefix: pointer.String("prefix"), 1067 }, 1068 }, 1069 structuredAuthnFeatureEnabled: true, 1070 want: `issuer.claimMappings.username.prefix: Invalid value: "prefix": prefix can't be set when expression is set`, 1071 }, 1072 { 1073 name: "username prefix is nil when claim is set", 1074 in: api.ClaimMappings{ 1075 Username: api.PrefixedClaimOrExpression{ 1076 Claim: "claim", 1077 }, 1078 }, 1079 structuredAuthnFeatureEnabled: true, 1080 want: `issuer.claimMappings.username.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`, 1081 }, 1082 { 1083 name: "username expression is invalid", 1084 in: api.ClaimMappings{ 1085 Username: api.PrefixedClaimOrExpression{ 1086 Expression: "foo.bar", 1087 }, 1088 }, 1089 structuredAuthnFeatureEnabled: true, 1090 want: `issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 1091 | foo.bar 1092 | ^`, 1093 }, 1094 { 1095 name: "groups expression and claim are set", 1096 in: api.ClaimMappings{ 1097 Username: api.PrefixedClaimOrExpression{ 1098 Claim: "claim", 1099 Prefix: pointer.String("prefix"), 1100 }, 1101 Groups: api.PrefixedClaimOrExpression{ 1102 Claim: "claim", 1103 Expression: "claims.groups", 1104 }, 1105 }, 1106 structuredAuthnFeatureEnabled: true, 1107 want: `issuer.claimMappings.groups: Invalid value: "": claim and expression can't both be set`, 1108 }, 1109 { 1110 name: "groups prefix set when expression is set", 1111 in: api.ClaimMappings{ 1112 Username: api.PrefixedClaimOrExpression{ 1113 Claim: "claim", 1114 Prefix: pointer.String("prefix"), 1115 }, 1116 Groups: api.PrefixedClaimOrExpression{ 1117 Expression: "claims.groups", 1118 Prefix: pointer.String("prefix"), 1119 }, 1120 }, 1121 structuredAuthnFeatureEnabled: true, 1122 want: `issuer.claimMappings.groups.prefix: Invalid value: "prefix": prefix can't be set when expression is set`, 1123 }, 1124 { 1125 name: "groups prefix is nil when claim is set", 1126 in: api.ClaimMappings{ 1127 Username: api.PrefixedClaimOrExpression{ 1128 Claim: "claim", 1129 Prefix: pointer.String("prefix"), 1130 }, 1131 Groups: api.PrefixedClaimOrExpression{ 1132 Claim: "claim", 1133 }, 1134 }, 1135 structuredAuthnFeatureEnabled: true, 1136 want: `issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`, 1137 }, 1138 { 1139 name: "groups expression is invalid", 1140 in: api.ClaimMappings{ 1141 Username: api.PrefixedClaimOrExpression{ 1142 Claim: "claim", 1143 Prefix: pointer.String("prefix"), 1144 }, 1145 Groups: api.PrefixedClaimOrExpression{ 1146 Expression: "foo.bar", 1147 }, 1148 }, 1149 structuredAuthnFeatureEnabled: true, 1150 want: `issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 1151 | foo.bar 1152 | ^`, 1153 }, 1154 { 1155 name: "uid claim and expression are set", 1156 in: api.ClaimMappings{ 1157 Username: api.PrefixedClaimOrExpression{ 1158 Claim: "claim", 1159 Prefix: pointer.String("prefix"), 1160 }, 1161 UID: api.ClaimOrExpression{ 1162 Claim: "claim", 1163 Expression: "claims.uid", 1164 }, 1165 }, 1166 structuredAuthnFeatureEnabled: true, 1167 want: `issuer.claimMappings.uid: Invalid value: "": claim and expression can't both be set`, 1168 }, 1169 { 1170 name: "uid expression is invalid", 1171 in: api.ClaimMappings{ 1172 Username: api.PrefixedClaimOrExpression{ 1173 Claim: "claim", 1174 Prefix: pointer.String("prefix"), 1175 }, 1176 UID: api.ClaimOrExpression{ 1177 Expression: "foo.bar", 1178 }, 1179 }, 1180 structuredAuthnFeatureEnabled: true, 1181 want: `issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 1182 | foo.bar 1183 | ^`, 1184 }, 1185 { 1186 name: "extra mapping key is empty", 1187 in: api.ClaimMappings{ 1188 Username: api.PrefixedClaimOrExpression{ 1189 Claim: "claim", 1190 Prefix: pointer.String("prefix"), 1191 }, 1192 Extra: []api.ExtraMapping{ 1193 {Key: "", ValueExpression: "claims.extra"}, 1194 }, 1195 }, 1196 structuredAuthnFeatureEnabled: true, 1197 want: `issuer.claimMappings.extra[0].key: Required value`, 1198 }, 1199 { 1200 name: "extra mapping value expression is empty", 1201 in: api.ClaimMappings{ 1202 Username: api.PrefixedClaimOrExpression{ 1203 Claim: "claim", 1204 Prefix: pointer.String("prefix"), 1205 }, 1206 Extra: []api.ExtraMapping{ 1207 {Key: "example.org/foo", ValueExpression: ""}, 1208 }, 1209 }, 1210 structuredAuthnFeatureEnabled: true, 1211 want: `issuer.claimMappings.extra[0].valueExpression: Required value: valueExpression is required`, 1212 }, 1213 { 1214 name: "extra mapping value expression is invalid", 1215 in: api.ClaimMappings{ 1216 Username: api.PrefixedClaimOrExpression{ 1217 Claim: "claim", 1218 Prefix: pointer.String("prefix"), 1219 }, 1220 Extra: []api.ExtraMapping{ 1221 {Key: "example.org/foo", ValueExpression: "foo.bar"}, 1222 }, 1223 }, 1224 structuredAuthnFeatureEnabled: true, 1225 want: `issuer.claimMappings.extra[0].valueExpression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 1226 | foo.bar 1227 | ^`, 1228 }, 1229 { 1230 name: "username expression is invalid when structured authn feature is disabled", 1231 in: api.ClaimMappings{ 1232 Username: api.PrefixedClaimOrExpression{ 1233 Expression: "foo.bar", 1234 }, 1235 }, 1236 structuredAuthnFeatureEnabled: false, 1237 want: `[issuer.claimMappings.username.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 1238 | foo.bar 1239 | ^]`, 1240 }, 1241 { 1242 name: "groups expression is invalid when structured authn feature is disabled", 1243 in: api.ClaimMappings{ 1244 Username: api.PrefixedClaimOrExpression{ 1245 Claim: "claim", 1246 Prefix: pointer.String("prefix"), 1247 }, 1248 Groups: api.PrefixedClaimOrExpression{ 1249 Expression: "foo.bar", 1250 }, 1251 }, 1252 structuredAuthnFeatureEnabled: false, 1253 want: `[issuer.claimMappings.groups.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 1254 | foo.bar 1255 | ^]`, 1256 }, 1257 { 1258 name: "uid expression is invalid when structured authn feature is disabled", 1259 in: api.ClaimMappings{ 1260 Username: api.PrefixedClaimOrExpression{ 1261 Claim: "claim", 1262 Prefix: pointer.String("prefix"), 1263 }, 1264 UID: api.ClaimOrExpression{ 1265 Expression: "foo.bar", 1266 }, 1267 }, 1268 structuredAuthnFeatureEnabled: false, 1269 want: `[issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 1270 | foo.bar 1271 | ^]`, 1272 }, 1273 { 1274 name: "uid claim is invalid when structured authn feature is disabled", 1275 in: api.ClaimMappings{ 1276 Username: api.PrefixedClaimOrExpression{ 1277 Claim: "claim", 1278 Prefix: pointer.String("prefix"), 1279 }, 1280 UID: api.ClaimOrExpression{ 1281 Claim: "claim", 1282 }, 1283 }, 1284 structuredAuthnFeatureEnabled: false, 1285 want: `issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`, 1286 }, 1287 { 1288 name: "extra mapping is invalid when structured authn feature is disabled", 1289 in: api.ClaimMappings{ 1290 Username: api.PrefixedClaimOrExpression{ 1291 Claim: "claim", 1292 Prefix: pointer.String("prefix"), 1293 }, 1294 Extra: []api.ExtraMapping{ 1295 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1296 }, 1297 }, 1298 structuredAuthnFeatureEnabled: false, 1299 want: `issuer.claimMappings.extra: Invalid value: "": extra claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`, 1300 }, 1301 { 1302 name: "duplicate extra mapping key", 1303 in: api.ClaimMappings{ 1304 Username: api.PrefixedClaimOrExpression{Expression: "claims.username"}, 1305 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1306 Extra: []api.ExtraMapping{ 1307 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1308 {Key: "example.org/foo", ValueExpression: "claims.extras"}, 1309 }, 1310 }, 1311 structuredAuthnFeatureEnabled: true, 1312 want: `issuer.claimMappings.extra[1].key: Duplicate value: "example.org/foo"`, 1313 }, 1314 { 1315 name: "extra mapping key is not domain prefix path", 1316 in: api.ClaimMappings{ 1317 Username: api.PrefixedClaimOrExpression{Expression: "claims.username"}, 1318 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1319 Extra: []api.ExtraMapping{ 1320 {Key: "foo", ValueExpression: "claims.extra"}, 1321 }, 1322 }, 1323 structuredAuthnFeatureEnabled: true, 1324 want: `issuer.claimMappings.extra[0].key: Invalid value: "foo": must be a domain-prefixed path (such as "acme.io/foo")`, 1325 }, 1326 { 1327 name: "extra mapping key is not lower case", 1328 in: api.ClaimMappings{ 1329 Username: api.PrefixedClaimOrExpression{Expression: "claims.username"}, 1330 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1331 Extra: []api.ExtraMapping{ 1332 {Key: "example.org/Foo", ValueExpression: "claims.extra"}, 1333 }, 1334 }, 1335 structuredAuthnFeatureEnabled: true, 1336 want: `issuer.claimMappings.extra[0].key: Invalid value: "example.org/Foo": key must be lowercase`, 1337 }, 1338 { 1339 name: "valid claim mappings but uses email without verification", 1340 in: api.ClaimMappings{ 1341 Username: api.PrefixedClaimOrExpression{Expression: "claims.email"}, 1342 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1343 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1344 Extra: []api.ExtraMapping{ 1345 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1346 }, 1347 }, 1348 structuredAuthnFeatureEnabled: true, 1349 wantCELMapper: true, 1350 want: `issuer.claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 1351 }, 1352 { 1353 name: "valid claim mappings but uses email in complex CEL expression without verification", 1354 in: api.ClaimMappings{ 1355 Username: api.PrefixedClaimOrExpression{Expression: "has(claims.email) ? claims.email : claims.sub"}, 1356 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1357 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1358 Extra: []api.ExtraMapping{ 1359 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1360 }, 1361 }, 1362 structuredAuthnFeatureEnabled: true, 1363 wantCELMapper: true, 1364 want: `issuer.claimMappings.username.expression: Invalid value: "has(claims.email) ? claims.email : claims.sub": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 1365 }, 1366 { 1367 name: "valid claim mappings but uses email in CEL expression function without verification", 1368 in: api.ClaimMappings{ 1369 Username: api.PrefixedClaimOrExpression{Expression: "claims.email.trim()"}, 1370 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1371 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1372 Extra: []api.ExtraMapping{ 1373 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1374 }, 1375 }, 1376 structuredAuthnFeatureEnabled: true, 1377 wantCELMapper: true, 1378 want: `issuer.claimMappings.username.expression: Invalid value: "claims.email.trim()": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 1379 }, 1380 { 1381 name: "valid claim mappings and uses email with verification via extra", 1382 in: api.ClaimMappings{ 1383 Username: api.PrefixedClaimOrExpression{Expression: "claims.email"}, 1384 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1385 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1386 Extra: []api.ExtraMapping{ 1387 {Key: "example.org/foo", ValueExpression: "claims.email_verified"}, 1388 }, 1389 }, 1390 structuredAuthnFeatureEnabled: true, 1391 wantCELMapper: true, 1392 want: "", 1393 }, 1394 { 1395 name: "valid claim mappings and uses email with verification via extra optional", 1396 in: api.ClaimMappings{ 1397 Username: api.PrefixedClaimOrExpression{Expression: "claims.email"}, 1398 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1399 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1400 Extra: []api.ExtraMapping{ 1401 {Key: "example.org/foo", ValueExpression: `has(claims.email_verified) ? string(claims.email_verified) : "false"`}, 1402 }, 1403 }, 1404 structuredAuthnFeatureEnabled: true, 1405 wantCELMapper: true, 1406 want: "", 1407 }, 1408 { 1409 name: "valid claim mappings and almost uses email with verification via extra optional", 1410 in: api.ClaimMappings{ 1411 Username: api.PrefixedClaimOrExpression{Expression: "claims.email"}, 1412 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1413 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1414 Extra: []api.ExtraMapping{ 1415 {Key: "example.org/foo", ValueExpression: `has(claims.email_verified_) ? string(claims.email_verified_) : "false"`}, 1416 }, 1417 }, 1418 structuredAuthnFeatureEnabled: true, 1419 wantCELMapper: true, 1420 want: `issuer.claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`, 1421 }, 1422 { 1423 name: "valid claim mappings and uses email with verification via hasVerifiedEmail", 1424 in: api.ClaimMappings{ 1425 Username: api.PrefixedClaimOrExpression{Expression: "claims.email"}, 1426 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1427 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1428 Extra: []api.ExtraMapping{ 1429 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1430 }, 1431 }, 1432 usesEmailVerifiedClaim: true, 1433 structuredAuthnFeatureEnabled: true, 1434 wantCELMapper: true, 1435 want: "", 1436 }, 1437 { 1438 name: "valid claim mappings that almost use claims.email", 1439 in: api.ClaimMappings{ 1440 Username: api.PrefixedClaimOrExpression{Expression: "claims.email_"}, 1441 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1442 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1443 Extra: []api.ExtraMapping{ 1444 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1445 }, 1446 }, 1447 structuredAuthnFeatureEnabled: true, 1448 wantCELMapper: true, 1449 want: "", 1450 }, 1451 { 1452 name: "valid claim mappings that almost use claims.email via nesting", 1453 in: api.ClaimMappings{ 1454 Username: api.PrefixedClaimOrExpression{Expression: "claims.other.claims.email"}, 1455 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1456 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1457 Extra: []api.ExtraMapping{ 1458 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1459 }, 1460 }, 1461 structuredAuthnFeatureEnabled: true, 1462 wantCELMapper: true, 1463 want: "", 1464 }, 1465 { 1466 name: "valid claim mappings", 1467 in: api.ClaimMappings{ 1468 Username: api.PrefixedClaimOrExpression{Expression: "claims.username"}, 1469 Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"}, 1470 UID: api.ClaimOrExpression{Expression: "claims.uid"}, 1471 Extra: []api.ExtraMapping{ 1472 {Key: "example.org/foo", ValueExpression: "claims.extra"}, 1473 }, 1474 }, 1475 structuredAuthnFeatureEnabled: true, 1476 wantCELMapper: true, 1477 want: "", 1478 }, 1479 } 1480 1481 for _, tt := range testCases { 1482 t.Run(tt.name, func(t *testing.T) { 1483 state := &validationState{usesEmailVerifiedClaim: tt.usesEmailVerifiedClaim} 1484 got := validateClaimMappings(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate() 1485 if d := cmp.Diff(tt.want, errString(got)); d != "" { 1486 fmt.Println(errString(got)) 1487 t.Fatalf("ClaimMappings validation mismatch (-want +got):\n%s", d) 1488 } 1489 if tt.wantCELMapper { 1490 if len(tt.in.Username.Expression) > 0 && state.mapper.Username == nil { 1491 t.Fatalf("ClaimMappings validation mismatch: CELMapper.Username is nil") 1492 } 1493 if len(tt.in.Groups.Expression) > 0 && state.mapper.Groups == nil { 1494 t.Fatalf("ClaimMappings validation mismatch: CELMapper.Groups is nil") 1495 } 1496 if len(tt.in.UID.Expression) > 0 && state.mapper.UID == nil { 1497 t.Fatalf("ClaimMappings validation mismatch: CELMapper.UID is nil") 1498 } 1499 if len(tt.in.Extra) > 0 && state.mapper.Extra == nil { 1500 t.Fatalf("ClaimMappings validation mismatch: CELMapper.Extra is nil") 1501 } 1502 } 1503 }) 1504 } 1505 } 1506 1507 func TestValidateUserValidationRules(t *testing.T) { 1508 fldPath := field.NewPath("issuer", "userValidationRules") 1509 1510 testCases := []struct { 1511 name string 1512 in []api.UserValidationRule 1513 structuredAuthnFeatureEnabled bool 1514 want string 1515 wantCELMapper bool 1516 }{ 1517 { 1518 name: "user info validation rule, expression is empty", 1519 in: []api.UserValidationRule{{}}, 1520 structuredAuthnFeatureEnabled: true, 1521 want: "issuer.userValidationRules[0].expression: Required value: expression is required", 1522 }, 1523 { 1524 name: "duplicate expression", 1525 in: []api.UserValidationRule{ 1526 {Expression: "user.username == 'foo'"}, 1527 {Expression: "user.username == 'foo'"}, 1528 }, 1529 structuredAuthnFeatureEnabled: true, 1530 want: `issuer.userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`, 1531 }, 1532 { 1533 name: "user validation rule is invalid when structured authn feature is disabled", 1534 in: []api.UserValidationRule{ 1535 {Expression: "user.username == 'foo'"}, 1536 }, 1537 structuredAuthnFeatureEnabled: false, 1538 want: `issuer.userValidationRules: Invalid value: "": user validation rules are not supported when StructuredAuthenticationConfiguration feature gate is disabled`, 1539 }, 1540 { 1541 name: "expression is invalid", 1542 in: []api.UserValidationRule{ 1543 {Expression: "foo.bar"}, 1544 }, 1545 structuredAuthnFeatureEnabled: true, 1546 want: `issuer.userValidationRules[0].expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '') 1547 | foo.bar 1548 | ^`, 1549 }, 1550 { 1551 name: "expression does not return bool", 1552 in: []api.UserValidationRule{ 1553 {Expression: "user.username"}, 1554 }, 1555 structuredAuthnFeatureEnabled: true, 1556 want: `issuer.userValidationRules[0].expression: Invalid value: "user.username": must evaluate to bool`, 1557 }, 1558 { 1559 name: "valid user info validation rule", 1560 in: []api.UserValidationRule{ 1561 {Expression: "user.username == 'foo'"}, 1562 {Expression: "!user.username.startsWith('system:')", Message: "username cannot used reserved system: prefix"}, 1563 }, 1564 structuredAuthnFeatureEnabled: true, 1565 want: "", 1566 wantCELMapper: true, 1567 }, 1568 } 1569 1570 for _, tt := range testCases { 1571 t.Run(tt.name, func(t *testing.T) { 1572 state := &validationState{} 1573 got := validateUserValidationRules(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate() 1574 if d := cmp.Diff(tt.want, errString(got)); d != "" { 1575 t.Fatalf("UserValidationRules validation mismatch (-want +got):\n%s", d) 1576 } 1577 if tt.wantCELMapper && state.mapper.UserValidationRules == nil { 1578 t.Fatalf("UserValidationRules validation mismatch: CELMapper.UserValidationRules is nil") 1579 } 1580 }) 1581 } 1582 } 1583 1584 func errString(errs errors.Aggregate) string { 1585 if errs != nil { 1586 return errs.Error() 1587 } 1588 return "" 1589 } 1590 1591 type ( 1592 test struct { 1593 name string 1594 configuration api.AuthorizationConfiguration 1595 expectedErrList field.ErrorList 1596 knownTypes sets.String 1597 repeatableTypes sets.String 1598 } 1599 ) 1600 1601 func TestValidateAuthorizationConfiguration(t *testing.T) { 1602 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true) 1603 1604 badKubeConfigFile := "../some/relative/path/kubeconfig" 1605 1606 tempKubeConfigFile, err := os.CreateTemp("/tmp", "kubeconfig") 1607 if err != nil { 1608 t.Fatalf("failed to set up temp file: %v", err) 1609 } 1610 tempKubeConfigFilePath := tempKubeConfigFile.Name() 1611 defer os.Remove(tempKubeConfigFilePath) 1612 1613 tests := []test{ 1614 { 1615 name: "atleast one authorizer should be defined", 1616 configuration: api.AuthorizationConfiguration{ 1617 Authorizers: []api.AuthorizerConfiguration{}, 1618 }, 1619 expectedErrList: field.ErrorList{field.Required(field.NewPath("authorizers"), "at least one authorization mode must be defined")}, 1620 knownTypes: sets.NewString(), 1621 repeatableTypes: sets.NewString(), 1622 }, 1623 { 1624 name: "type and name are required if an authorizer is defined", 1625 configuration: api.AuthorizationConfiguration{ 1626 Authorizers: []api.AuthorizerConfiguration{ 1627 {}, 1628 }, 1629 }, 1630 expectedErrList: field.ErrorList{field.Required(field.NewPath("type"), "")}, 1631 knownTypes: sets.NewString(string("Webhook")), 1632 repeatableTypes: sets.NewString(string("Webhook")), 1633 }, 1634 { 1635 name: "authorizer names should be of non-zero length", 1636 configuration: api.AuthorizationConfiguration{ 1637 Authorizers: []api.AuthorizerConfiguration{ 1638 { 1639 Type: "Foo", 1640 Name: "", 1641 }, 1642 }, 1643 }, 1644 expectedErrList: field.ErrorList{field.Required(field.NewPath("name"), "")}, 1645 knownTypes: sets.NewString(string("Foo")), 1646 repeatableTypes: sets.NewString(string("Webhook")), 1647 }, 1648 { 1649 name: "authorizer names should be unique", 1650 configuration: api.AuthorizationConfiguration{ 1651 Authorizers: []api.AuthorizerConfiguration{ 1652 { 1653 Type: "Foo", 1654 Name: "foo", 1655 }, 1656 { 1657 Type: "Bar", 1658 Name: "foo", 1659 }, 1660 }, 1661 }, 1662 expectedErrList: field.ErrorList{field.Duplicate(field.NewPath("name"), "foo")}, 1663 knownTypes: sets.NewString(string("Foo"), string("Bar")), 1664 repeatableTypes: sets.NewString(string("Webhook")), 1665 }, 1666 { 1667 name: "authorizer names should be DNS1123 labels", 1668 configuration: api.AuthorizationConfiguration{ 1669 Authorizers: []api.AuthorizerConfiguration{ 1670 { 1671 Type: "Foo", 1672 Name: "myauthorizer", 1673 }, 1674 }, 1675 }, 1676 expectedErrList: field.ErrorList{}, 1677 knownTypes: sets.NewString(string("Foo")), 1678 repeatableTypes: sets.NewString(string("Webhook")), 1679 }, 1680 { 1681 name: "authorizer names should be DNS1123 subdomains", 1682 configuration: api.AuthorizationConfiguration{ 1683 Authorizers: []api.AuthorizerConfiguration{ 1684 { 1685 Type: "Foo", 1686 Name: "foo.example.domain", 1687 }, 1688 }, 1689 }, 1690 expectedErrList: field.ErrorList{}, 1691 knownTypes: sets.NewString(string("Foo")), 1692 repeatableTypes: sets.NewString(string("Webhook")), 1693 }, 1694 { 1695 name: "authorizer names should not be invalid DNS1123 labels or subdomains", 1696 configuration: api.AuthorizationConfiguration{ 1697 Authorizers: []api.AuthorizerConfiguration{ 1698 { 1699 Type: "Foo", 1700 Name: "FOO.example.domain", 1701 }, 1702 }, 1703 }, 1704 expectedErrList: field.ErrorList{field.Invalid(field.NewPath("name"), "FOO.example.domain", "")}, 1705 knownTypes: sets.NewString(string("Foo")), 1706 repeatableTypes: sets.NewString(string("Webhook")), 1707 }, 1708 { 1709 name: "bare minimum configuration with Webhook", 1710 configuration: api.AuthorizationConfiguration{ 1711 Authorizers: []api.AuthorizerConfiguration{ 1712 { 1713 Type: "Webhook", 1714 Name: "default", 1715 Webhook: &api.WebhookConfiguration{ 1716 Timeout: metav1.Duration{Duration: 5 * time.Second}, 1717 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 1718 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1719 FailurePolicy: "NoOpinion", 1720 SubjectAccessReviewVersion: "v1", 1721 MatchConditionSubjectAccessReviewVersion: "v1", 1722 ConnectionInfo: api.WebhookConnectionInfo{ 1723 Type: "InClusterConfig", 1724 }, 1725 }, 1726 }, 1727 }, 1728 }, 1729 expectedErrList: field.ErrorList{}, 1730 knownTypes: sets.NewString(string("Webhook")), 1731 repeatableTypes: sets.NewString(string("Webhook")), 1732 }, 1733 { 1734 name: "bare minimum configuration with Webhook and MatchConditions", 1735 configuration: api.AuthorizationConfiguration{ 1736 Authorizers: []api.AuthorizerConfiguration{ 1737 { 1738 Type: "Webhook", 1739 Name: "default", 1740 Webhook: &api.WebhookConfiguration{ 1741 Timeout: metav1.Duration{Duration: 5 * time.Second}, 1742 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 1743 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1744 FailurePolicy: "NoOpinion", 1745 SubjectAccessReviewVersion: "v1", 1746 MatchConditionSubjectAccessReviewVersion: "v1", 1747 ConnectionInfo: api.WebhookConnectionInfo{ 1748 Type: "InClusterConfig", 1749 }, 1750 MatchConditions: []api.WebhookMatchCondition{ 1751 { 1752 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'", 1753 }, 1754 { 1755 Expression: "request.user == 'admin'", 1756 }, 1757 }, 1758 }, 1759 }, 1760 }, 1761 }, 1762 expectedErrList: field.ErrorList{}, 1763 knownTypes: sets.NewString(string("Webhook")), 1764 repeatableTypes: sets.NewString(string("Webhook")), 1765 }, 1766 { 1767 name: "bare minimum configuration with multiple webhooks", 1768 configuration: api.AuthorizationConfiguration{ 1769 Authorizers: []api.AuthorizerConfiguration{ 1770 { 1771 Type: "Webhook", 1772 Name: "default", 1773 Webhook: &api.WebhookConfiguration{ 1774 Timeout: metav1.Duration{Duration: 5 * time.Second}, 1775 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 1776 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1777 FailurePolicy: "NoOpinion", 1778 SubjectAccessReviewVersion: "v1", 1779 MatchConditionSubjectAccessReviewVersion: "v1", 1780 ConnectionInfo: api.WebhookConnectionInfo{ 1781 Type: "InClusterConfig", 1782 }, 1783 }, 1784 }, 1785 { 1786 Type: "Webhook", 1787 Name: "second-webhook", 1788 Webhook: &api.WebhookConfiguration{ 1789 Timeout: metav1.Duration{Duration: 5 * time.Second}, 1790 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 1791 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1792 FailurePolicy: "NoOpinion", 1793 SubjectAccessReviewVersion: "v1", 1794 MatchConditionSubjectAccessReviewVersion: "v1", 1795 ConnectionInfo: api.WebhookConnectionInfo{ 1796 Type: "InClusterConfig", 1797 }, 1798 }, 1799 }, 1800 }, 1801 }, 1802 expectedErrList: field.ErrorList{}, 1803 knownTypes: sets.NewString(string("Webhook")), 1804 repeatableTypes: sets.NewString(string("Webhook")), 1805 }, 1806 { 1807 name: "configuration with unknown types", 1808 configuration: api.AuthorizationConfiguration{ 1809 Authorizers: []api.AuthorizerConfiguration{ 1810 { 1811 Type: "Foo", 1812 }, 1813 }, 1814 }, 1815 expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("type"), "Foo", []string{"..."})}, 1816 knownTypes: sets.NewString(string("Webhook")), 1817 repeatableTypes: sets.NewString(string("Webhook")), 1818 }, 1819 { 1820 name: "configuration with not repeatable types", 1821 configuration: api.AuthorizationConfiguration{ 1822 Authorizers: []api.AuthorizerConfiguration{ 1823 { 1824 Type: "Foo", 1825 Name: "foo-1", 1826 }, 1827 { 1828 Type: "Foo", 1829 Name: "foo-2", 1830 }, 1831 }, 1832 }, 1833 expectedErrList: field.ErrorList{field.Duplicate(field.NewPath("type"), "Foo")}, 1834 knownTypes: sets.NewString(string("Foo")), 1835 repeatableTypes: sets.NewString(string("Webhook")), 1836 }, 1837 { 1838 name: "when type=Webhook, webhook needs to be defined", 1839 configuration: api.AuthorizationConfiguration{ 1840 Authorizers: []api.AuthorizerConfiguration{ 1841 { 1842 Type: "Webhook", 1843 Name: "default", 1844 }, 1845 }, 1846 }, 1847 expectedErrList: field.ErrorList{field.Required(field.NewPath("webhook"), "required when type=Webhook")}, 1848 knownTypes: sets.NewString(string("Webhook")), 1849 repeatableTypes: sets.NewString(string("Webhook")), 1850 }, 1851 { 1852 name: "when type!=Webhook, webhooks needs to be nil", 1853 configuration: api.AuthorizationConfiguration{ 1854 Authorizers: []api.AuthorizerConfiguration{ 1855 { 1856 Type: "Foo", 1857 Name: "foo", 1858 Webhook: &api.WebhookConfiguration{}, 1859 }, 1860 }, 1861 }, 1862 expectedErrList: field.ErrorList{field.Invalid(field.NewPath("webhook"), "non-null", "may only be specified when type=Webhook")}, 1863 knownTypes: sets.NewString(string("Foo")), 1864 repeatableTypes: sets.NewString(string("Webhook")), 1865 }, 1866 { 1867 name: "timeout should be specified", 1868 configuration: api.AuthorizationConfiguration{ 1869 Authorizers: []api.AuthorizerConfiguration{ 1870 { 1871 Type: "Webhook", 1872 Name: "default", 1873 Webhook: &api.WebhookConfiguration{ 1874 FailurePolicy: "NoOpinion", 1875 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 1876 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1877 SubjectAccessReviewVersion: "v1", 1878 MatchConditionSubjectAccessReviewVersion: "v1", 1879 ConnectionInfo: api.WebhookConnectionInfo{ 1880 Type: "InClusterConfig", 1881 }, 1882 }, 1883 }, 1884 }, 1885 }, 1886 expectedErrList: field.ErrorList{field.Required(field.NewPath("timeout"), "")}, 1887 knownTypes: sets.NewString(string("Webhook")), 1888 repeatableTypes: sets.NewString(string("Webhook")), 1889 }, 1890 // 1891 { 1892 name: "timeout shouldn't be zero", 1893 configuration: api.AuthorizationConfiguration{ 1894 Authorizers: []api.AuthorizerConfiguration{ 1895 { 1896 Type: "Webhook", 1897 Name: "default", 1898 Webhook: &api.WebhookConfiguration{ 1899 FailurePolicy: "NoOpinion", 1900 Timeout: metav1.Duration{Duration: 0 * time.Second}, 1901 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 1902 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1903 SubjectAccessReviewVersion: "v1", 1904 MatchConditionSubjectAccessReviewVersion: "v1", 1905 ConnectionInfo: api.WebhookConnectionInfo{ 1906 Type: "InClusterConfig", 1907 }, 1908 }, 1909 }, 1910 }, 1911 }, 1912 expectedErrList: field.ErrorList{field.Required(field.NewPath("timeout"), "")}, 1913 knownTypes: sets.NewString(string("Webhook")), 1914 repeatableTypes: sets.NewString(string("Webhook")), 1915 }, 1916 { 1917 name: "timeout shouldn't be negative", 1918 configuration: api.AuthorizationConfiguration{ 1919 Authorizers: []api.AuthorizerConfiguration{ 1920 { 1921 Type: "Webhook", 1922 Name: "default", 1923 Webhook: &api.WebhookConfiguration{ 1924 FailurePolicy: "NoOpinion", 1925 Timeout: metav1.Duration{Duration: -30 * time.Second}, 1926 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 1927 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1928 SubjectAccessReviewVersion: "v1", 1929 MatchConditionSubjectAccessReviewVersion: "v1", 1930 ConnectionInfo: api.WebhookConnectionInfo{ 1931 Type: "InClusterConfig", 1932 }, 1933 }, 1934 }, 1935 }, 1936 }, 1937 expectedErrList: field.ErrorList{field.Invalid(field.NewPath("timeout"), time.Duration(-30*time.Second).String(), "must be > 0s and <= 30s")}, 1938 knownTypes: sets.NewString(string("Webhook")), 1939 repeatableTypes: sets.NewString(string("Webhook")), 1940 }, 1941 { 1942 name: "timeout shouldn't be greater than 30seconds", 1943 configuration: api.AuthorizationConfiguration{ 1944 Authorizers: []api.AuthorizerConfiguration{ 1945 { 1946 Type: "Webhook", 1947 Name: "default", 1948 Webhook: &api.WebhookConfiguration{ 1949 FailurePolicy: "NoOpinion", 1950 Timeout: metav1.Duration{Duration: 60 * time.Second}, 1951 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 1952 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1953 SubjectAccessReviewVersion: "v1", 1954 MatchConditionSubjectAccessReviewVersion: "v1", 1955 ConnectionInfo: api.WebhookConnectionInfo{ 1956 Type: "InClusterConfig", 1957 }, 1958 }, 1959 }, 1960 }, 1961 }, 1962 expectedErrList: field.ErrorList{field.Invalid(field.NewPath("timeout"), time.Duration(60*time.Second).String(), "must be > 0s and <= 30s")}, 1963 knownTypes: sets.NewString(string("Webhook")), 1964 repeatableTypes: sets.NewString(string("Webhook")), 1965 }, 1966 { 1967 name: "authorizedTTL should be defined ", 1968 configuration: api.AuthorizationConfiguration{ 1969 Authorizers: []api.AuthorizerConfiguration{ 1970 { 1971 Type: "Webhook", 1972 Name: "default", 1973 Webhook: &api.WebhookConfiguration{ 1974 FailurePolicy: "NoOpinion", 1975 Timeout: metav1.Duration{Duration: 5 * time.Second}, 1976 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 1977 SubjectAccessReviewVersion: "v1", 1978 MatchConditionSubjectAccessReviewVersion: "v1", 1979 ConnectionInfo: api.WebhookConnectionInfo{ 1980 Type: "InClusterConfig", 1981 }, 1982 }, 1983 }, 1984 }, 1985 }, 1986 expectedErrList: field.ErrorList{field.Required(field.NewPath("authorizedTTL"), "")}, 1987 knownTypes: sets.NewString(string("Webhook")), 1988 repeatableTypes: sets.NewString(string("Webhook")), 1989 }, 1990 { 1991 name: "authorizedTTL shouldn't be negative", 1992 configuration: api.AuthorizationConfiguration{ 1993 Authorizers: []api.AuthorizerConfiguration{ 1994 { 1995 Type: "Webhook", 1996 Name: "default", 1997 Webhook: &api.WebhookConfiguration{ 1998 FailurePolicy: "NoOpinion", 1999 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2000 AuthorizedTTL: metav1.Duration{Duration: -30 * time.Second}, 2001 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2002 SubjectAccessReviewVersion: "v1", 2003 MatchConditionSubjectAccessReviewVersion: "v1", 2004 ConnectionInfo: api.WebhookConnectionInfo{ 2005 Type: "InClusterConfig", 2006 }, 2007 }, 2008 }, 2009 }, 2010 }, 2011 expectedErrList: field.ErrorList{field.Invalid(field.NewPath("authorizedTTL"), time.Duration(-30*time.Second).String(), "must be > 0s")}, 2012 knownTypes: sets.NewString(string("Webhook")), 2013 repeatableTypes: sets.NewString(string("Webhook")), 2014 }, 2015 { 2016 name: "unauthorizedTTL should be defined ", 2017 configuration: api.AuthorizationConfiguration{ 2018 Authorizers: []api.AuthorizerConfiguration{ 2019 { 2020 Type: "Webhook", 2021 Name: "default", 2022 Webhook: &api.WebhookConfiguration{ 2023 FailurePolicy: "NoOpinion", 2024 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2025 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2026 SubjectAccessReviewVersion: "v1", 2027 MatchConditionSubjectAccessReviewVersion: "v1", 2028 ConnectionInfo: api.WebhookConnectionInfo{ 2029 Type: "InClusterConfig", 2030 }, 2031 }, 2032 }, 2033 }, 2034 }, 2035 expectedErrList: field.ErrorList{field.Required(field.NewPath("unauthorizedTTL"), "")}, 2036 knownTypes: sets.NewString(string("Webhook")), 2037 repeatableTypes: sets.NewString(string("Webhook")), 2038 }, 2039 { 2040 name: "unauthorizedTTL shouldn't be negative", 2041 configuration: api.AuthorizationConfiguration{ 2042 Authorizers: []api.AuthorizerConfiguration{ 2043 { 2044 Type: "Webhook", 2045 Name: "default", 2046 Webhook: &api.WebhookConfiguration{ 2047 FailurePolicy: "NoOpinion", 2048 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2049 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2050 UnauthorizedTTL: metav1.Duration{Duration: -30 * time.Second}, 2051 SubjectAccessReviewVersion: "v1", 2052 MatchConditionSubjectAccessReviewVersion: "v1", 2053 ConnectionInfo: api.WebhookConnectionInfo{ 2054 Type: "InClusterConfig", 2055 }, 2056 }, 2057 }, 2058 }, 2059 }, 2060 expectedErrList: field.ErrorList{field.Invalid(field.NewPath("unauthorizedTTL"), time.Duration(-30*time.Second).String(), "must be > 0s")}, 2061 knownTypes: sets.NewString(string("Webhook")), 2062 repeatableTypes: sets.NewString(string("Webhook")), 2063 }, 2064 { 2065 name: "SAR should be defined", 2066 configuration: api.AuthorizationConfiguration{ 2067 Authorizers: []api.AuthorizerConfiguration{ 2068 { 2069 Type: "Webhook", 2070 Name: "default", 2071 Webhook: &api.WebhookConfiguration{ 2072 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2073 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2074 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2075 MatchConditionSubjectAccessReviewVersion: "v1", 2076 FailurePolicy: "NoOpinion", 2077 ConnectionInfo: api.WebhookConnectionInfo{ 2078 Type: "InClusterConfig", 2079 }, 2080 }, 2081 }, 2082 }, 2083 }, 2084 expectedErrList: field.ErrorList{field.Required(field.NewPath("subjectAccessReviewVersion"), "")}, 2085 knownTypes: sets.NewString(string("Webhook")), 2086 repeatableTypes: sets.NewString(string("Webhook")), 2087 }, 2088 { 2089 name: "SAR should be one of v1 and v1beta1", 2090 configuration: api.AuthorizationConfiguration{ 2091 Authorizers: []api.AuthorizerConfiguration{ 2092 { 2093 Type: "Webhook", 2094 Name: "default", 2095 Webhook: &api.WebhookConfiguration{ 2096 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2097 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2098 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2099 FailurePolicy: "NoOpinion", 2100 SubjectAccessReviewVersion: "v2beta1", 2101 MatchConditionSubjectAccessReviewVersion: "v1", 2102 ConnectionInfo: api.WebhookConnectionInfo{ 2103 Type: "InClusterConfig", 2104 }, 2105 }, 2106 }, 2107 }, 2108 }, 2109 expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("subjectAccessReviewVersion"), "v2beta1", []string{"v1", "v1beta1"})}, 2110 knownTypes: sets.NewString(string("Webhook")), 2111 repeatableTypes: sets.NewString(string("Webhook")), 2112 }, 2113 { 2114 name: "MatchConditionSAR should be defined", 2115 configuration: api.AuthorizationConfiguration{ 2116 Authorizers: []api.AuthorizerConfiguration{ 2117 { 2118 Type: "Webhook", 2119 Name: "default", 2120 Webhook: &api.WebhookConfiguration{ 2121 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2122 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2123 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2124 FailurePolicy: "NoOpinion", 2125 SubjectAccessReviewVersion: "v1", 2126 ConnectionInfo: api.WebhookConnectionInfo{ 2127 Type: "InClusterConfig", 2128 }, 2129 MatchConditions: []api.WebhookMatchCondition{{Expression: "true"}}, 2130 }, 2131 }, 2132 }, 2133 }, 2134 expectedErrList: field.ErrorList{field.Required(field.NewPath("matchConditionSubjectAccessReviewVersion"), "")}, 2135 knownTypes: sets.NewString(string("Webhook")), 2136 repeatableTypes: sets.NewString(string("Webhook")), 2137 }, 2138 { 2139 name: "MatchConditionSAR must not be anything other than v1", 2140 configuration: api.AuthorizationConfiguration{ 2141 Authorizers: []api.AuthorizerConfiguration{ 2142 { 2143 Type: "Webhook", 2144 Name: "default", 2145 Webhook: &api.WebhookConfiguration{ 2146 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2147 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2148 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2149 FailurePolicy: "NoOpinion", 2150 SubjectAccessReviewVersion: "v1", 2151 MatchConditionSubjectAccessReviewVersion: "v1beta1", 2152 ConnectionInfo: api.WebhookConnectionInfo{ 2153 Type: "InClusterConfig", 2154 }, 2155 }, 2156 }, 2157 }, 2158 }, 2159 expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("matchConditionSubjectAccessReviewVersion"), "v1beta1", []string{"v1"})}, 2160 knownTypes: sets.NewString(string("Webhook")), 2161 repeatableTypes: sets.NewString(string("Webhook")), 2162 }, 2163 { 2164 name: "failurePolicy should be defined", 2165 configuration: api.AuthorizationConfiguration{ 2166 Authorizers: []api.AuthorizerConfiguration{ 2167 { 2168 Type: "Webhook", 2169 Name: "default", 2170 Webhook: &api.WebhookConfiguration{ 2171 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2172 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2173 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2174 SubjectAccessReviewVersion: "v1", 2175 MatchConditionSubjectAccessReviewVersion: "v1", 2176 ConnectionInfo: api.WebhookConnectionInfo{ 2177 Type: "InClusterConfig", 2178 }, 2179 }, 2180 }, 2181 }, 2182 }, 2183 expectedErrList: field.ErrorList{field.Required(field.NewPath("failurePolicy"), "")}, 2184 knownTypes: sets.NewString(string("Webhook")), 2185 repeatableTypes: sets.NewString(string("Webhook")), 2186 }, 2187 { 2188 name: "failurePolicy should be one of \"NoOpinion\" or \"Deny\"", 2189 configuration: api.AuthorizationConfiguration{ 2190 Authorizers: []api.AuthorizerConfiguration{ 2191 { 2192 Type: "Webhook", 2193 Name: "default", 2194 Webhook: &api.WebhookConfiguration{ 2195 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2196 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2197 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2198 FailurePolicy: "AlwaysAllow", 2199 SubjectAccessReviewVersion: "v1", 2200 MatchConditionSubjectAccessReviewVersion: "v1", 2201 ConnectionInfo: api.WebhookConnectionInfo{ 2202 Type: "InClusterConfig", 2203 }, 2204 }, 2205 }, 2206 }, 2207 }, 2208 expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("failurePolicy"), "AlwaysAllow", []string{"NoOpinion", "Deny"})}, 2209 knownTypes: sets.NewString(string("Webhook")), 2210 repeatableTypes: sets.NewString(string("Webhook")), 2211 }, 2212 { 2213 name: "connectionInfo should be defined", 2214 configuration: api.AuthorizationConfiguration{ 2215 Authorizers: []api.AuthorizerConfiguration{ 2216 { 2217 Type: "Webhook", 2218 Name: "default", 2219 Webhook: &api.WebhookConfiguration{ 2220 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2221 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2222 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2223 FailurePolicy: "NoOpinion", 2224 SubjectAccessReviewVersion: "v1", 2225 MatchConditionSubjectAccessReviewVersion: "v1", 2226 }, 2227 }, 2228 }, 2229 }, 2230 expectedErrList: field.ErrorList{field.Required(field.NewPath("connectionInfo"), "")}, 2231 knownTypes: sets.NewString(string("Webhook")), 2232 repeatableTypes: sets.NewString(string("Webhook")), 2233 }, 2234 { 2235 name: "connectionInfo should be one of InClusterConfig or KubeConfigFile", 2236 configuration: api.AuthorizationConfiguration{ 2237 Authorizers: []api.AuthorizerConfiguration{ 2238 { 2239 Type: "Webhook", 2240 Name: "default", 2241 Webhook: &api.WebhookConfiguration{ 2242 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2243 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2244 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2245 FailurePolicy: "NoOpinion", 2246 SubjectAccessReviewVersion: "v1", 2247 MatchConditionSubjectAccessReviewVersion: "v1", 2248 ConnectionInfo: api.WebhookConnectionInfo{ 2249 Type: "ExternalClusterConfig", 2250 }, 2251 }, 2252 }, 2253 }, 2254 }, 2255 expectedErrList: field.ErrorList{ 2256 field.NotSupported(field.NewPath("connectionInfo"), api.WebhookConnectionInfo{Type: "ExternalClusterConfig"}, []string{"InClusterConfig", "KubeConfigFile"}), 2257 }, 2258 knownTypes: sets.NewString(string("Webhook")), 2259 repeatableTypes: sets.NewString(string("Webhook")), 2260 }, 2261 { 2262 name: "if connectionInfo=InClusterConfig, then kubeConfigFile should be nil", 2263 configuration: api.AuthorizationConfiguration{ 2264 Authorizers: []api.AuthorizerConfiguration{ 2265 { 2266 Type: "Webhook", 2267 Name: "default", 2268 Webhook: &api.WebhookConfiguration{ 2269 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2270 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2271 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2272 FailurePolicy: "NoOpinion", 2273 SubjectAccessReviewVersion: "v1", 2274 MatchConditionSubjectAccessReviewVersion: "v1", 2275 ConnectionInfo: api.WebhookConnectionInfo{ 2276 Type: "InClusterConfig", 2277 KubeConfigFile: new(string), 2278 }, 2279 }, 2280 }, 2281 }, 2282 }, 2283 expectedErrList: field.ErrorList{ 2284 field.Invalid(field.NewPath("connectionInfo", "kubeConfigFile"), "", "can only be set when type=KubeConfigFile"), 2285 }, 2286 knownTypes: sets.NewString(string("Webhook")), 2287 repeatableTypes: sets.NewString(string("Webhook")), 2288 }, 2289 { 2290 name: "if connectionInfo=KubeConfigFile, then KubeConfigFile should be defined", 2291 configuration: api.AuthorizationConfiguration{ 2292 Authorizers: []api.AuthorizerConfiguration{ 2293 { 2294 Type: "Webhook", 2295 Name: "default", 2296 Webhook: &api.WebhookConfiguration{ 2297 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2298 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2299 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2300 FailurePolicy: "NoOpinion", 2301 SubjectAccessReviewVersion: "v1", 2302 MatchConditionSubjectAccessReviewVersion: "v1", 2303 ConnectionInfo: api.WebhookConnectionInfo{ 2304 Type: "KubeConfigFile", 2305 }, 2306 }, 2307 }, 2308 }, 2309 }, 2310 expectedErrList: field.ErrorList{field.Required(field.NewPath("kubeConfigFile"), "")}, 2311 knownTypes: sets.NewString(string("Webhook")), 2312 repeatableTypes: sets.NewString(string("Webhook")), 2313 }, 2314 { 2315 name: "if connectionInfo=KubeConfigFile, then KubeConfigFile should be defined, must be an absolute path, should exist, shouldn't be a symlink", 2316 configuration: api.AuthorizationConfiguration{ 2317 Authorizers: []api.AuthorizerConfiguration{ 2318 { 2319 Type: "Webhook", 2320 Name: "default", 2321 Webhook: &api.WebhookConfiguration{ 2322 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2323 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2324 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2325 FailurePolicy: "NoOpinion", 2326 SubjectAccessReviewVersion: "v1", 2327 MatchConditionSubjectAccessReviewVersion: "v1", 2328 ConnectionInfo: api.WebhookConnectionInfo{ 2329 Type: "KubeConfigFile", 2330 KubeConfigFile: &badKubeConfigFile, 2331 }, 2332 }, 2333 }, 2334 }, 2335 }, 2336 expectedErrList: field.ErrorList{field.Invalid(field.NewPath("kubeConfigFile"), badKubeConfigFile, "must be an absolute path")}, 2337 knownTypes: sets.NewString(string("Webhook")), 2338 repeatableTypes: sets.NewString(string("Webhook")), 2339 }, 2340 { 2341 name: "if connectionInfo=KubeConfigFile, an existent file needs to be passed", 2342 configuration: api.AuthorizationConfiguration{ 2343 Authorizers: []api.AuthorizerConfiguration{ 2344 { 2345 Type: "Webhook", 2346 Name: "default", 2347 Webhook: &api.WebhookConfiguration{ 2348 Timeout: metav1.Duration{Duration: 5 * time.Second}, 2349 AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute}, 2350 UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second}, 2351 FailurePolicy: "NoOpinion", 2352 SubjectAccessReviewVersion: "v1", 2353 MatchConditionSubjectAccessReviewVersion: "v1", 2354 ConnectionInfo: api.WebhookConnectionInfo{ 2355 Type: "KubeConfigFile", 2356 KubeConfigFile: &tempKubeConfigFilePath, 2357 }, 2358 }, 2359 }, 2360 }, 2361 }, 2362 expectedErrList: field.ErrorList{}, 2363 knownTypes: sets.NewString(string("Webhook")), 2364 repeatableTypes: sets.NewString(string("Webhook")), 2365 }, 2366 } 2367 2368 for _, test := range tests { 2369 t.Run(test.name, func(t *testing.T) { 2370 errList := ValidateAuthorizationConfiguration(nil, &test.configuration, test.knownTypes, test.repeatableTypes) 2371 if len(errList) != len(test.expectedErrList) { 2372 t.Errorf("expected %d errs, got %d, errors %v", len(test.expectedErrList), len(errList), errList) 2373 } 2374 if len(errList) == len(test.expectedErrList) { 2375 for i, expected := range test.expectedErrList { 2376 if expected.Type.String() != errList[i].Type.String() { 2377 t.Errorf("expected err type %s, got %s", 2378 expected.Type.String(), 2379 errList[i].Type.String()) 2380 } 2381 if expected.BadValue != errList[i].BadValue { 2382 t.Errorf("expected bad value '%s', got '%s'", 2383 expected.BadValue, 2384 errList[i].BadValue) 2385 } 2386 } 2387 } 2388 }) 2389 2390 } 2391 } 2392 2393 func TestValidateAndCompileMatchConditions(t *testing.T) { 2394 testCases := []struct { 2395 name string 2396 matchConditions []api.WebhookMatchCondition 2397 featureEnabled bool 2398 expectedErr string 2399 }{ 2400 { 2401 name: "match conditions are used With feature enabled", 2402 matchConditions: []api.WebhookMatchCondition{ 2403 { 2404 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'", 2405 }, 2406 { 2407 Expression: "request.user == 'admin'", 2408 }, 2409 }, 2410 featureEnabled: true, 2411 expectedErr: "", 2412 }, 2413 { 2414 name: "should fail when match conditions are used without feature enabled", 2415 matchConditions: []api.WebhookMatchCondition{ 2416 { 2417 Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'", 2418 }, 2419 { 2420 Expression: "request.user == 'admin'", 2421 }, 2422 }, 2423 featureEnabled: false, 2424 expectedErr: `matchConditions: Invalid value: "": matchConditions are not supported when StructuredAuthorizationConfiguration feature gate is disabled`, 2425 }, 2426 { 2427 name: "no matchConditions should not require feature enablement", 2428 matchConditions: []api.WebhookMatchCondition{}, 2429 featureEnabled: false, 2430 expectedErr: "", 2431 }, 2432 { 2433 name: "match conditions with invalid expressions", 2434 matchConditions: []api.WebhookMatchCondition{ 2435 { 2436 Expression: " ", 2437 }, 2438 }, 2439 featureEnabled: true, 2440 expectedErr: "matchConditions[0].expression: Required value", 2441 }, 2442 { 2443 name: "match conditions with duplicate expressions", 2444 matchConditions: []api.WebhookMatchCondition{ 2445 { 2446 Expression: "request.user == 'admin'", 2447 }, 2448 { 2449 Expression: "request.user == 'admin'", 2450 }, 2451 }, 2452 featureEnabled: true, 2453 expectedErr: `matchConditions[1].expression: Duplicate value: "request.user == 'admin'"`, 2454 }, 2455 { 2456 name: "match conditions with undeclared reference", 2457 matchConditions: []api.WebhookMatchCondition{ 2458 { 2459 Expression: "test", 2460 }, 2461 }, 2462 featureEnabled: true, 2463 expectedErr: "matchConditions[0].expression: Invalid value: \"test\": compilation failed: ERROR: <input>:1:1: undeclared reference to 'test' (in container '')\n | test\n | ^", 2464 }, 2465 { 2466 name: "match conditions with bad return type", 2467 matchConditions: []api.WebhookMatchCondition{ 2468 { 2469 Expression: "request.user = 'test'", 2470 }, 2471 }, 2472 featureEnabled: true, 2473 expectedErr: "matchConditions[0].expression: Invalid value: \"request.user = 'test'\": compilation failed: ERROR: <input>:1:14: Syntax error: token recognition error at: '= '\n | request.user = 'test'\n | .............^\nERROR: <input>:1:16: Syntax error: extraneous input ''test'' expecting <EOF>\n | request.user = 'test'\n | ...............^", 2474 }, 2475 } 2476 2477 for _, tt := range testCases { 2478 t.Run(tt.name, func(t *testing.T) { 2479 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, tt.featureEnabled) 2480 celMatcher, errList := ValidateAndCompileMatchConditions(tt.matchConditions) 2481 if len(tt.expectedErr) == 0 && len(tt.matchConditions) > 0 && len(errList) == 0 && celMatcher == nil { 2482 t.Errorf("celMatcher should not be nil when there are matchCondition and no error returned") 2483 } 2484 got := errList.ToAggregate() 2485 if d := cmp.Diff(tt.expectedErr, errString(got)); d != "" { 2486 t.Fatalf("ValidateAndCompileMatchConditions validation mismatch (-want +got):\n%s", d) 2487 } 2488 }) 2489 } 2490 }