Change:
For example, should not let you set discovery URL when the feature gate is not enabled. I believe this only impacts library users, as direct usage in KAS requires the file based config, which is feature gated. /assign enj aramase /milestone v1.32

Discussion:
compiler := authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) state := &validationState{} <del> allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"))...) </del> <ins> allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"), structuredAuthnFeatureEnabled)...) </ins> allErrs = append(allErrs, validateClaimValidationRules(compiler, state, authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"), structuredAuthnFeatureEnabled)...) allErrs = append(allErrs, validateClaimMappings(compiler, state, authenticator.ClaimMappings, fldPath.Child("claimMappings"), structuredAuthnFeatureEnabled)...) allErrs = append(allErrs, validateUserValidationRules(compiler, state, authenticator.UserValidationRules, fldPath.Child("userValidationRules"), structuredAuthnFeatureEnabled)...)

usesEmailVerifiedClaim bool } <del> func validateIssuer(issuer api.Issuer, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList { </del> <ins> func validateIssuer(issuer api.Issuer, disallowedIssuers sets.Set[string], fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList { </ins> var allErrs field.ErrorList allErrs = append(allErrs, validateIssuerURL(issuer.URL, disallowedIssuers, fldPath.Child("url"))...) <del> allErrs = append(allErrs, validateIssuerDiscoveryURL(issuer.URL, issuer.DiscoveryURL, fldPath.Child("discoveryURL"))...) allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"))...) </del> <ins> allErrs = append(allErrs, validateIssuerDiscoveryURL(issuer.URL, issuer.DiscoveryURL, fldPath.Child("discoveryURL"), structuredAuthnFeatureEnabled)...) allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"), structuredAuthnFeatureEnabled)...) </ins> allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...) return allErrs

return validateURL(issuerURL, disallowedIssuers, fldPath) } <del> func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *field.Path) field.ErrorList { </del> <ins> func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList { </ins> var allErrs field.ErrorList if len(issuerDiscoveryURL) == 0 { return nil } <ins> if !structuredAuthnFeatureEnabled { allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL is not supported when StructuredAuthenticationConfiguration feature gate is disabled")) } </ins> if len(issuerURL) > 0 && strings.TrimRight(issuerURL, "/") == strings.TrimRight(issuerDiscoveryURL, "/") { allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL must be different from URL")) }

return allErrs } <del> func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatchPolicyType, fldPath, audienceMatchPolicyFldPath *field.Path) field.ErrorList { </del> <ins> func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatchPolicyType, fldPath, audienceMatchPolicyFldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList { </ins> var allErrs field.ErrorList if len(audiences) == 0 {

return allErrs } <ins> if len(audiences) > 1 && !structuredAuthnFeatureEnabled { allErrs = append(allErrs, field.Invalid(fldPath, audiences, "multiple audiences are not supported when StructuredAuthenticationConfiguration feature gate is disabled")) } </ins> seenAudiences := sets.NewString() for i, audience := range audiences { fldPath := fldPath.Index(i)

fldPath := field.NewPath("issuer", "discoveryURL") testCases := []struct { <del> name      string in        string issuerURL string want      string </del> <ins> name                          string in                            string issuerURL                     string want                          string structuredAuthnFeatureEnabled bool </ins> }{ { <del> name: "url is empty", in:   "", want: "", </del> <ins> name:                          "url is empty", in:                            "", want:                          "", structuredAuthnFeatureEnabled: true, </ins> }, { <del> name: "url parse error", in:   "https://oidc.oidc-namespace.svc:invalid-port", 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`, </del> <ins> name:                          "url parse error", in:                            "https://oidc.oidc-namespace.svc:invalid-port", 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`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name: "url is not https", in:   "http://oidc.oidc-namespace.svc", want: `issuer.discoveryURL: Invalid value: "http://oidc.oidc-namespace.svc": URL scheme must be https`, </del> <ins> name:                          "url is not https", in:                            "http://oidc.oidc-namespace.svc", want:                          `issuer.discoveryURL: Invalid value: "http://oidc.oidc-namespace.svc": URL scheme must be https`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name: "url user info is not allowed", in:   "https://user:pass@oidc.oidc-namespace.svc", want: `issuer.discoveryURL: Invalid value: "https://user:pass@oidc.oidc-namespace.svc": URL must not contain a username or password`, </del> <ins> name:                          "url user info is not allowed", in:                            "https://user:pass@oidc.oidc-namespace.svc", want:                          `issuer.discoveryURL: Invalid value: "https://user:pass@oidc.oidc-namespace.svc": URL must not contain a username or password`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name: "url raw query is not allowed", in:   "https://oidc.oidc-namespace.svc?query", want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc?query": URL must not contain a query`, </del> <ins> name:                          "url raw query is not allowed", in:                            "https://oidc.oidc-namespace.svc?query", want:                          `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc?query": URL must not contain a query`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name: "url fragment is not allowed", in:   "https://oidc.oidc-namespace.svc#fragment", want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc#fragment": URL must not contain a fragment`, </del> <ins> name:                          "url fragment is not allowed", in:                            "https://oidc.oidc-namespace.svc#fragment", want:                          `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc#fragment": URL must not contain a fragment`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name: "valid url", in:   "https://oidc.oidc-namespace.svc", want: "", </del> <ins> name:                          "valid url", in:                            "https://oidc.oidc-namespace.svc", want:                          "", structuredAuthnFeatureEnabled: true, </ins> }, { <del> name: "valid url with path", in:   "https://oidc.oidc-namespace.svc/path", want: "", </del> <ins> name:                          "valid url with path", in:                            "https://oidc.oidc-namespace.svc/path", want:                          "", structuredAuthnFeatureEnabled: true, </ins> }, { <del> name:      "discovery url same as issuer url", issuerURL: "https://issuer-url", in:        "https://issuer-url", want:      `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`, </del> <ins> name:                          "discovery url same as issuer url", issuerURL:                     "https://issuer-url", in:                            "https://issuer-url", want:                          `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name:      "discovery url same as issuer url, with trailing slash", issuerURL: "https://issuer-url", in:        "https://issuer-url/", want:      `issuer.discoveryURL: Invalid value: "https://issuer-url/": discoveryURL must be different from URL`, </del> <ins> name:                          "discovery url same as issuer url, with trailing slash", issuerURL:                     "https://issuer-url", in:                            "https://issuer-url/", want:                          `issuer.discoveryURL: Invalid value: "https://issuer-url/": discoveryURL must be different from URL`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name:      "discovery url same as issuer url, with multiple trailing slashes", issuerURL: "https://issuer-url", in:        "https://issuer-url///", want:      `issuer.discoveryURL: Invalid value: "https://issuer-url///": discoveryURL must be different from URL`, </del> <ins> name:                          "discovery url same as issuer url, with multiple trailing slashes", issuerURL:                     "https://issuer-url", in:                            "https://issuer-url///", want:                          `issuer.discoveryURL: Invalid value: "https://issuer-url///": discoveryURL must be different from URL`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name:      "discovery url same as issuer url, issuer url with trailing slash", issuerURL: "https://issuer-url/", in:        "https://issuer-url", want:      `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`, </del> <ins> name:                          "discovery url same as issuer url, issuer url with trailing slash", issuerURL:                     "https://issuer-url/", in:                            "https://issuer-url", want:                          `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`, structuredAuthnFeatureEnabled: true, }, { name: "discovery url set but structured authn feature disabled", in:   "https://oidc.oidc-namespace.svc", want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc": discoveryURL is not supported when StructuredAuthenticationConfiguration feature gate is disabled`, </ins> }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { <del> got := validateIssuerDiscoveryURL(tt.issuerURL, tt.in, fldPath).ToAggregate() </del> <ins> got := validateIssuerDiscoveryURL(tt.issuerURL, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate() </ins> if d := cmp.Diff(tt.want, errString(got)); d != "" { t.Fatalf("URL validation mismatch (-want +got):n%s", d) }

audienceMatchPolicyFldPath := field.NewPath("issuer", "audienceMatchPolicy") testCases := []struct { <del> name        string in          []string matchPolicy string want        string </del> <ins> name                          string in                            []string matchPolicy                   string want                          string structuredAuthnFeatureEnabled bool </ins> }{ { name: "audiences is empty",

want:        "", }, { <del> name:        "duplicate audience", in:          []string{"audience", "audience"}, matchPolicy: "MatchAny", want:        `issuer.audiences[1]: Duplicate value: "audience"`, </del> <ins> name:                          "duplicate audience", in:                            []string{"audience", "audience"}, matchPolicy:                   "MatchAny", want:                          `issuer.audiences[1]: Duplicate value: "audience"`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name: "match policy not set with multiple audiences", in:   []string{"audience1", "audience2"}, want: `issuer.audienceMatchPolicy: Invalid value: "": audienceMatchPolicy must be MatchAny for multiple audiences`, </del> <ins> name:                          "match policy not set with multiple audiences", in:                            []string{"audience1", "audience2"}, want:                          `issuer.audienceMatchPolicy: Invalid value: "": audienceMatchPolicy must be MatchAny for multiple audiences`, structuredAuthnFeatureEnabled: true, </ins> }, { <del> name:        "valid multiple audiences", </del> <ins> name:                          "valid multiple audiences", in:                            []string{"audience1", "audience2"}, matchPolicy:                   "MatchAny", want:                          "", structuredAuthnFeatureEnabled: true, }, { name:        "multiple audiences set when structured authn feature is disabled", </ins> in:          []string{"audience1", "audience2"}, matchPolicy: "MatchAny", <del> want:        "", </del> <ins> want:        `issuer.audiences: Invalid value: []string{"audience1", "audience2"}: multiple audiences are not supported when StructuredAuthenticationConfiguration feature gate is disabled`, </ins> }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { <del> got := validateAudiences(tt.in, api.AudienceMatchPolicyType(tt.matchPolicy), fldPath, audienceMatchPolicyFldPath).ToAggregate() </del> <ins> got := validateAudiences(tt.in, api.AudienceMatchPolicyType(tt.matchPolicy), fldPath, audienceMatchPolicyFldPath, tt.structuredAuthnFeatureEnabled).ToAggregate() </ins> if d := cmp.Diff(tt.want, errString(got)); d != "" { t.Fatalf("Audiences validation mismatch (-want +got):n%s", d) }