From 810a5370e7d9d3a6be7123261002def0655f51cf Mon Sep 17 00:00:00 2001 From: Shaza Aldawamneh Date: Mon, 17 Nov 2025 12:05:20 +0100 Subject: [PATCH 1/3] adding the API new fields to the cluster-auth-operator Signed-off-by: Shaza Aldawamneh --- go.mod | 2 + go.sum | 4 +- .../externaloidc/externaloidc_controller.go | 50 +++- .../externaloidc_controller_test.go | 116 +++++++-- pkg/operator/starter.go | 2 +- .../api/config/v1/types_authentication.go | 141 ++++++++-- .../api/config/v1/types_cluster_version.go | 2 +- .../api/config/v1/zz_generated.deepcopy.go | 38 +++ ..._generated.featuregated-crd-manifests.yaml | 1 + .../v1/zz_generated.swagger_doc_generated.go | 31 ++- vendor/github.com/openshift/api/features.md | 5 +- .../openshift/api/features/features.go | 26 +- .../api/machine/v1beta1/types_gcpprovider.go | 16 -- .../machine/v1beta1/zz_generated.deepcopy.go | 5 - .../zz_generated.swagger_doc_generated.go | 1 - .../operator/v1/types_csi_cluster_driver.go | 1 + ...clustercsidrivers-CustomNoUpgrade.crd.yaml | 1 + ...iver_01_clustercsidrivers-Default.crd.yaml | 1 + ...tercsidrivers-DevPreviewNoUpgrade.crd.yaml | 1 + ...ercsidrivers-TechPreviewNoUpgrade.crd.yaml | 1 + ...nfigurations-TechPreviewNoUpgrade.crd.yaml | 245 ------------------ vendor/modules.txt | 3 +- 22 files changed, 361 insertions(+), 332 deletions(-) diff --git a/go.mod b/go.mod index 9a8431425..53f260d04 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,8 @@ require ( sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 ) +replace github.com/openshift/api => github.com/ShazaAldawamneh/api v0.0.0-20251105172922-8847fb59bcff + require ( cel.dev/expr v0.24.0 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect diff --git a/go.sum b/go.sum index 0006cbcca..05f6184dc 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEs github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= github.com/RangelReale/osincli v0.0.0-20160924135400-fababb0555f2 h1:x8Brv0YNEe6jY3V/hQglIG2nd8g5E2Zj5ubGKkPQctQ= github.com/RangelReale/osincli v0.0.0-20160924135400-fababb0555f2/go.mod h1:XyjUkMA8GN+tOOPXvnbi3XuRxWFvTJntqvTFnjmhzbk= +github.com/ShazaAldawamneh/api v0.0.0-20251105172922-8847fb59bcff h1:HvT7pgcTmEsgNLgyCPaB/znHfwdZd8MBwYwbj3GEqMc= +github.com/ShazaAldawamneh/api v0.0.0-20251105172922-8847fb59bcff/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -147,8 +149,6 @@ github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/openshift/api v0.0.0-20251106190826-ebe535b08719 h1:KEwYyKaJniwhoyLB75tAMmJn9pMlk0PUlRfrsXYOhwM= -github.com/openshift/api v0.0.0-20251106190826-ebe535b08719/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+Sp5GGnjHDhT/a/nQ1xdp43UscBMr7G5wxsYotyhzJ4= github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 h1:9JBeIXmnHlpXTQPi7LPmu1jdxznBhAE7bb1K+3D8gxY= diff --git a/pkg/controllers/externaloidc/externaloidc_controller.go b/pkg/controllers/externaloidc/externaloidc_controller.go index 55982dd5a..e80204f9c 100644 --- a/pkg/controllers/externaloidc/externaloidc_controller.go +++ b/pkg/controllers/externaloidc/externaloidc_controller.go @@ -193,9 +193,15 @@ func generateJWTForProvider(provider configv1.OIDCProvider, configMapLister core return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating claimValidationRules for provider %q: %v", provider.Name, err) } + userValidationRules, err := generateUserValidationRules(provider.UserValidationRules) + if err != nil { + return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating userValidationRules for provider %q: %v", provider.Name, err) + } + out.Issuer = issuer out.ClaimMappings = claimMappings out.ClaimValidationRules = claimValidationRules + out.UserValidationRules = userValidationRules return out, nil } @@ -216,6 +222,10 @@ func generateIssuer(issuer configv1.TokenIssuer, configMapLister corev1listers.C out.Audiences = append(out.Audiences, string(audience)) } + if issuer.DiscoveryURL != "" { + out.DiscoveryURL = &issuer.DiscoveryURL + } + if len(issuer.CertificateAuthority.Name) > 0 { ca, err := getCertificateAuthorityFromConfigMap(issuer.CertificateAuthority.Name, configMapLister) if err != nil { @@ -421,13 +431,28 @@ func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidati // validation rule and does not allow setting a CEL expression and message like the upstream. // This is likely to change in the near future to also allow setting a CEL expression. switch claimValidationRule.Type { - case configv1.TokenValidationRuleTypeRequiredClaim: + case configv1.TokenValidationRuleRequiredClaim: if claimValidationRule.RequiredClaim == nil { - return apiserverv1beta1.ClaimValidationRule{}, fmt.Errorf("claimValidationRule.type is %s and requiredClaim is not set", configv1.TokenValidationRuleTypeRequiredClaim) + return apiserverv1beta1.ClaimValidationRule{}, fmt.Errorf("claimValidationRule.type is %s and requiredClaim is not set", configv1.TokenValidationRuleRequiredClaim) } out.Claim = claimValidationRule.RequiredClaim.Claim out.RequiredValue = claimValidationRule.RequiredClaim.RequiredValue + case configv1.TokenValidationRuleExpression: + if claimValidationRule.Expression.Expression == "" { + return apiserverv1beta1.ClaimValidationRule{}, fmt.Errorf("claimValidationRule.type is %s and expression is not set", configv1.TokenValidationRuleExpression) + } + + // validate CEL expression + if err := validateCELExpression(&authenticationcel.ClaimMappingExpression{ + Expression: claimValidationRule.Expression.Expression, + }); err != nil { + return apiserverv1beta1.ClaimValidationRule{}, fmt.Errorf("invalid CEL expression: %v", err) + } + + out.Expression = claimValidationRule.Expression.Expression + out.Message = claimValidationRule.Expression.Message + default: return apiserverv1beta1.ClaimValidationRule{}, fmt.Errorf("unknown claimValidationRule type %q", claimValidationRule.Type) } @@ -435,6 +460,27 @@ func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidati return out, nil } +func generateUserValidationRules(rules []configv1.TokenUserValidationRule) ([]apiserverv1beta1.UserValidationRule, error) { + out := []apiserverv1beta1.UserValidationRule{} + errs := []error{} + for _, r := range rules { + // Validate the expression is non-empty + if len(r.Expression) == 0 { + errs = append(errs, fmt.Errorf("userValidationRule expression must be non-empty")) + continue + } + uvr := apiserverv1beta1.UserValidationRule{ + Expression: r.Expression, + Message: r.Message, + } + out = append(out, uvr) + } + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + return out, nil +} + // getExpectedApplyConfig serializes the input authConfig into JSON and creates an apply configuration // for a configmap with the serialized authConfig in the right key. func getExpectedApplyConfig(authConfig apiserverv1beta1.AuthenticationConfiguration) (*corev1ac.ConfigMapApplyConfiguration, error) { diff --git a/pkg/controllers/externaloidc/externaloidc_controller_test.go b/pkg/controllers/externaloidc/externaloidc_controller_test.go index e915e3db5..1f16960ee 100644 --- a/pkg/controllers/externaloidc/externaloidc_controller_test.go +++ b/pkg/controllers/externaloidc/externaloidc_controller_test.go @@ -63,6 +63,7 @@ var ( Issuer: configv1.TokenIssuer{ CertificateAuthority: configv1.ConfigMapNameReference{Name: "oidc-ca-bundle"}, Audiences: []configv1.TokenAudience{"my-test-aud", "another-aud"}, + DiscoveryURL: "https://example.com/.well-known/openid-configuration", }, OIDCClients: []configv1.OIDCClientConfig{ { @@ -93,20 +94,26 @@ var ( }, ClaimValidationRules: []configv1.TokenClaimValidationRule{ { - Type: configv1.TokenValidationRuleTypeRequiredClaim, + Type: configv1.TokenValidationRuleRequiredClaim, RequiredClaim: &configv1.TokenRequiredClaim{ Claim: "username", RequiredValue: "test-username", }, }, { - Type: configv1.TokenValidationRuleTypeRequiredClaim, - RequiredClaim: &configv1.TokenRequiredClaim{ - Claim: "email", - RequiredValue: "test-email", + Type: configv1.TokenValidationRuleExpression, + Expression: configv1.TokenExpressionRule{ + Expression: "claims.email.endsWith('@example.com')", + Message: "email domain must be example.com", }, }, }, + UserValidationRules: []configv1.TokenUserValidationRule{ + { + Expression: "user.groups.exists(g, g == 'system:authenticated')", + Message: "user must be authenticated", + }, + }, }, }, }) @@ -122,6 +129,7 @@ var ( Audiences: []string{"my-test-aud", "another-aud"}, CertificateAuthority: testCertData, AudienceMatchPolicy: apiserverv1beta1.AudienceMatchPolicyMatchAny, + DiscoveryURL: ptr.To("https://example.com/.well-known/openid-configuration"), }, ClaimMappings: apiserverv1beta1.ClaimMappings{ Username: apiserverv1beta1.PrefixedClaimOrExpression{ @@ -139,17 +147,22 @@ var ( RequiredValue: "test-username", }, { - Claim: "email", - RequiredValue: "test-email", + Expression: "claims.email.endsWith('@example.com')", + Message: "email domain must be example.com", + }, + }, + UserValidationRules: []apiserverv1beta1.UserValidationRule{ + { + Expression: "user.groups.exists(g, g == 'system:authenticated')", + Message: "user must be authenticated", }, }, }, }, } - baseAuthConfigJSON = fmt.Sprintf(`{"kind":"%s","apiVersion":"apiserver.config.k8s.io/v1beta1","jwt":[{"issuer":{"url":"$URL","certificateAuthority":"%s","audiences":["my-test-aud","another-aud"],"audienceMatchPolicy":"MatchAny"},"claimValidationRules":[{"claim":"username","requiredValue":"test-username"},{"claim":"email","requiredValue":"test-email"}],"claimMappings":{"username":{"claim":"username","prefix":"oidc-user:"},"groups":{"claim":"groups","prefix":"oidc-group:"},"uid":{}}}]}`, kindAuthenticationConfiguration, strings.ReplaceAll(testCertData, "\n", "\\n")) - - baseAuthConfigCM = corev1.ConfigMap{ + baseAuthConfigJSON = fmt.Sprintf(`{"kind":"%s","apiVersion":"apiserver.config.k8s.io/v1beta1","jwt":[{"issuer":{"url":"$URL","certificateAuthority":"%s","audiences":["my-test-aud","another-aud"],"audienceMatchPolicy":"MatchAny"},"claimValidationRules":[{"claim":"username","requiredValue":"test-username"},{"claim":"email","requiredValue":"test-email"},{"expression":"claims.email.endsWith('@example.com')","message":"email domain must be example.com"}],"userValidationRules":[{"expression":"user.groups.exists(g, g == 'system:authenticated')","message":"user must be authenticated"}],"claimMappings":{"username":{"claim":"username","prefix":"oidc-user:"},"groups":{"claim":"groups","prefix":"oidc-group:"},"uid":{}}}]}`, kindAuthenticationConfiguration, strings.ReplaceAll(testCertData, "\n", "\\n")) + baseAuthConfigCM = corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: targetAuthConfigCMName, Namespace: managedNamespace, @@ -690,10 +703,13 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { if len(copy.Spec.OIDCProviders[i].ClaimValidationRules) == 0 { copy.Spec.OIDCProviders[i].ClaimValidationRules = make([]configv1.TokenClaimValidationRule, 0) } - copy.Spec.OIDCProviders[i].ClaimValidationRules = append(copy.Spec.OIDCProviders[i].ClaimValidationRules, configv1.TokenClaimValidationRule{ - Type: configv1.TokenValidationRuleTypeRequiredClaim, - RequiredClaim: nil, - }) + copy.Spec.OIDCProviders[i].ClaimValidationRules = append( + copy.Spec.OIDCProviders[i].ClaimValidationRules, + configv1.TokenClaimValidationRule{ + Type: configv1.TokenValidationRuleRequiredClaim, // updated constant + RequiredClaim: nil, + }, + ) } }, }), @@ -701,8 +717,7 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { featureGates: featuregates.NewFeatureGate( []configv1.FeatureGateName{}, []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - }, + features.FeatureGateExternalOIDCWithUpstreamParity}, ), }, { @@ -1056,6 +1071,75 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { []configv1.FeatureGateName{}, ), }, + { + name: "invalid discovery URL (empty)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "" + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + }, + ), + }, + { + name: "invalid discovery URL (http instead of https)", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].Issuer.URL = "http://insecure-url.com" + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + }, + ), + }, + { + name: "claim validation rule missing required claim", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].ClaimValidationRules = append( + auth.Spec.OIDCProviders[0].ClaimValidationRules, + configv1.TokenClaimValidationRule{ + Type: configv1.TokenValidationRuleRequiredClaim, + RequiredClaim: nil, + }, + ) + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithUpstreamParity, + }, + ), + }, + { + name: "user validation rule invalid username prefix", + auth: *authWithUpdates(baseAuthResource, []func(auth *configv1.Authentication){ + func(auth *configv1.Authentication) { + auth.Spec.OIDCProviders[0].ClaimMappings.Username = configv1.UsernameClaimMapping{ + Claim: "username", + PrefixPolicy: configv1.UsernamePrefixPolicy("invalid-policy"), + } + }, + }), + expectError: true, + featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{}, + []configv1.FeatureGateName{ + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + }, + ), + }, } { t.Run(tt.name, func(t *testing.T) { if tt.configMapIndexer == nil { diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index 21e7d2aa8..9b540ef01 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -773,7 +773,7 @@ func prepareExternalOIDC( return nil, nil, fmt.Errorf("timed out waiting for FeatureGate detection") } - if !(featureGates.Enabled(features.FeatureGateExternalOIDC) || featureGates.Enabled(features.FeatureGateExternalOIDCWithAdditionalClaimMappings)) { + if !(featureGates.Enabled(features.FeatureGateExternalOIDC) || featureGates.Enabled(features.FeatureGateExternalOIDCWithAdditionalClaimMappings) || featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity)) { return nil, nil, nil } diff --git a/vendor/github.com/openshift/api/config/v1/types_authentication.go b/vendor/github.com/openshift/api/config/v1/types_authentication.go index 52a41b2fe..9aff27edb 100644 --- a/vendor/github.com/openshift/api/config/v1/types_authentication.go +++ b/vendor/github.com/openshift/api/config/v1/types_authentication.go @@ -5,7 +5,7 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDC;ExternalOIDCWithUIDAndExtraClaimMappings,rule="!has(self.spec.oidcProviders) || self.spec.oidcProviders.all(p, !has(p.oidcClients) || p.oidcClients.all(specC, self.status.oidcClients.exists(statusC, statusC.componentNamespace == specC.componentNamespace && statusC.componentName == specC.componentName) || (has(oldSelf.spec.oidcProviders) && oldSelf.spec.oidcProviders.exists(oldP, oldP.name == p.name && has(oldP.oidcClients) && oldP.oidcClients.exists(oldC, oldC.componentNamespace == specC.componentNamespace && oldC.componentName == specC.componentName)))))",message="all oidcClients in the oidcProviders must match their componentName and componentNamespace to either a previously configured oidcClient or they must exist in the status.oidcClients" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDC;ExternalOIDCWithUIDAndExtraClaimMappings;ExternalOIDCWithUpstreamParity,rule="!has(self.spec.oidcProviders) || self.spec.oidcProviders.all(p, !has(p.oidcClients) || p.oidcClients.all(specC, self.status.oidcClients.exists(statusC, statusC.componentNamespace == specC.componentNamespace && statusC.componentName == specC.componentName) || (has(oldSelf.spec.oidcProviders) && oldSelf.spec.oidcProviders.exists(oldP, oldP.name == p.name && has(oldP.oidcClients) && oldP.oidcClients.exists(oldC, oldC.componentNamespace == specC.componentNamespace && oldC.componentName == specC.componentName)))))",message="all oidcClients in the oidcProviders must match their componentName and componentNamespace to either a previously configured oidcClient or they must exist in the status.oidcClients" // Authentication specifies cluster-wide settings for authentication (like OAuth and // webhook token authenticators). The canonical name of an instance is `cluster`. @@ -91,6 +91,7 @@ type AuthenticationSpec struct { // +kubebuilder:validation:MaxItems=1 // +openshift:enable:FeatureGate=ExternalOIDC // +openshift:enable:FeatureGate=ExternalOIDCWithUIDAndExtraClaimMappings + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity // +optional OIDCProviders []OIDCProvider `json:"oidcProviders,omitempty"` } @@ -243,11 +244,29 @@ type OIDCProvider struct { // +listType=atomic // +optional ClaimValidationRules []TokenClaimValidationRule `json:"claimValidationRules,omitempty"` + + // userValidationRules defines the set of rules used to validate claims in a user's token. + // Each rule is evaluated independently to determine whether the token subject is considered valid. + // Rules can either require specific claims and values to be present, + // or define CEL expressions that must evaluate to true for the token to be accepted. + // If the expression in a rule evaluates to false, the token is rejected. + // At least one rule must evaluate to true for the token to be considered valid. + // A maximum of 64 rules can be specified. This field is optional. + // + // See https://kubernetes.io/docs/reference/using-api/cel/ for CEL syntax. + // +listType=atomic + // +kubebuilder:validation:MaxItems=64 + // +listType=map + // +listMapKey=expression + // +optional + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + UserValidationRules []TokenUserValidationRule `json:"userValidationRules,omitempty"` } // +kubebuilder:validation:MinLength=1 type TokenAudience string +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDCWithUpstreamParity,rule="self.?discoveryURL.orValue(\"\").size() > 0 ? (self.issuerURL.size() == 0 || self.discoveryURL.find('^.+[^/]') != self.issuerURL.find('^.+[^/]')) : true",message="discoveryURL must be different from issuerURL" type TokenIssuer struct { // issuerURL is a required field that configures the URL used to issue tokens // by the identity provider. @@ -291,6 +310,26 @@ type TokenIssuer struct { // // +optional CertificateAuthority ConfigMapNameReference `json:"issuerCertificateAuthority"` + // discoveryURL is an optional field that, if specified, overrides the default discovery endpoint + // used to retrieve OIDC configuration metadata. By default, the discovery URL is derived from `issuerURL` + // as "{url}/.well-known/openid-configuration". + // + // The discoveryURL must: + // - Be a valid absolute URL. + // - Use the HTTPS scheme. + // - Not contain query parameters, user info, or fragments. + // - Be different from the value of `url` (ignoring trailing slashes) + // + // +optional + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + // +kubebuilder:validation:XValidation:rule="isURL(self)",message="discoveryURL must be a valid URL" + // +kubebuilder:validation:XValidation:rule="url(self).getScheme() == 'https'",message="discoveryURL must be a valid https URL" + // +kubebuilder:validation:XValidation:rule="url(self).getQuery().size() == 0",message="discoveryURL must not contain query parameters" + // +kubebuilder:validation:XValidation:rule="self.matches('^[^#]*$')",message="discoveryURL must not contain fragments" + // +kubebuilder:validation:XValidation:rule="!(self.matches('^https:\\/\\/.+:.+@{1}.+\\/.*$'))",message="discoveryURL must not contain user info" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + DiscoveryURL string `json:"discoveryURL,omitempty"` } type TokenClaimMappings struct { @@ -717,45 +756,64 @@ type PrefixedClaimMapping struct { Prefix string `json:"prefix"` } -// TokenValidationRuleType represents the different -// claim validation rule types that can be configured. +// TokenValidationRuleType defines the type of token validation rule. // +enum +// +openshift:validation:FeatureGateAwareEnum:featureGate="",enum="RequiredClaim"; +// +openshift:validation:FeatureGateAwareEnum:featureGate=ExternalOIDCWithUpstreamParity,enum="RequiredClaim";"Expression" +// +required type TokenValidationRuleType string const ( - TokenValidationRuleTypeRequiredClaim = "RequiredClaim" + // TokenValidationRuleRequiredClaim indicates that the token must contain a specific claim. + // Used as a value for TokenValidationRuleType. + TokenValidationRuleRequiredClaim = "RequiredClaim" + // TokenValidationRuleExpression indicates that the token validation is defined via a CEL expression. + // Used as a value for TokenValidationRuleType. + TokenValidationRuleExpression = "Expression" ) +// TokenClaimValidationRule represents a validation rule based on token claims. +// If type is RequiredClaim, requiredClaim must be set. +// If type is Expression, expression must be set. +// +// +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'RequiredClaim' ? has(self.requiredClaim) : !has(self.requiredClaim)",message="requiredClaim must be set when type is 'RequiredClaim', and forbidden otherwise" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDCWithUpstreamParity,rule="has(self.type) && self.type == 'Expression' ? has(self.expression) : !has(self.expression)",message="expression must be set when type is 'Expression', and forbidden otherwise" type TokenClaimValidationRule struct { // type is an optional field that configures the type of the validation rule. // - // Allowed values are 'RequiredClaim' and omitted (not provided or an empty string). - // - // When set to 'RequiredClaim', the Kubernetes API server - // will be configured to validate that the incoming JWT - // contains the required claim and that its value matches - // the required value. + // Allowed values are "RequiredClaim" and "Expression". // - // Defaults to 'RequiredClaim'. + // When set to 'RequiredClaim', the Kubernetes API server will be configured + // to validate that the incoming JWT contains the required claim and that its + // value matches the required value. // - // +kubebuilder:validation:Enum={"RequiredClaim"} - // +kubebuilder:default="RequiredClaim" + // When set to 'Expression', the Kubernetes API server will be configured + // to validate the incoming JWT against the configured CEL expression. Type TokenValidationRuleType `json:"type"` - // requiredClaim is an optional field that configures the required claim - // and value that the Kubernetes API server will use to validate if an incoming - // JWT is valid for this identity provider. + // requiredClaim allows configuring a required claim name and its expected value. + // When type is RequiredClaim, this field is used by the Kubernetes API server + // to validate if an incoming JWT is valid for this identity provider. // // +optional RequiredClaim *TokenRequiredClaim `json:"requiredClaim,omitempty"` + + // expression configures a CEL expression that will be used + // by the Kubernetes API server to validate if an incoming JWT + // is valid for this identity provider. The CEL expression must + // return a boolean value where 'true' signals a valid state. + // Expression must be set when 'type' is 'Expression', and + // is forbidden otherwise. + // + // +optional + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + Expression TokenExpressionRule `json:"expression,omitzero,omitempty"` } type TokenRequiredClaim struct { - // claim is a required field that configures the name of the required claim. // When taken from the JWT claims, claim must be a string value. // // claim must not be an empty string (""). - // // +kubebuilder:validation:MinLength=1 // +required Claim string `json:"claim"` @@ -771,3 +829,50 @@ type TokenRequiredClaim struct { // +required RequiredValue string `json:"requiredValue"` } + +type TokenExpressionRule struct { + // expression is a CEL expression evaluated against token claims. + // The expression must be a non-empty string and no longer than 1024 characters. + // The expression must return a boolean value where 'true' signals a valid token and 'false' an invalid one. + // This field is required. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + // +required + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + Expression string `json:"expression,omitempty"` + + // message allows configuring a human-readable message that is logged by the Kubernetes API server + // when a token fails validation based on the CEL expression defined in 'Expression'. + // This field is optional. If provided, the message must be at least 1 character long + // and cannot exceed 256 characters. This message is logged and not returned to the caller. + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + Message string `json:"message,omitempty"` +} + +// TokenUserValidationRule provides a CEL-based rule used to validate a token subject. +// Each rule contains a CEL expression that is evaluated against the token’s claims. +type TokenUserValidationRule struct { + // expression is a CEL expression that must evaluate + // to true for the token to be accepted. The expression is evaluated against the token's + // user information (e.g., username, groups). + // This field must be non-empty and may not exceed 1024 characters. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + Expression string `json:"expression,omitempty"` + // message allows configuring a human-readable message that is logged by the Kubernetes API server + // when a token fails validation based on the CEL expression defined in 'Expression'. + // This field is optional. If provided, the message must be at least 1 character long + // and cannot exceed 256 characters. This message is logged and not returned to the caller. + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + Message string `json:"message,omitempty"` +} diff --git a/vendor/github.com/openshift/api/config/v1/types_cluster_version.go b/vendor/github.com/openshift/api/config/v1/types_cluster_version.go index e5aad151e..7929f4b62 100644 --- a/vendor/github.com/openshift/api/config/v1/types_cluster_version.go +++ b/vendor/github.com/openshift/api/config/v1/types_cluster_version.go @@ -727,7 +727,7 @@ type Update struct { // operator and you have verified the authenticity of the provided // image yourself. // The provided image will run with full administrative access - // to the cluster. Do not use this flag with images that come from unknown + // to the cluster. Do not use this flag with images that comes from unknown // or potentially malicious sources. // // +optional diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go index 0863934f2..c8745dde3 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.deepcopy.go @@ -4753,6 +4753,11 @@ func (in *OIDCProvider) DeepCopyInto(out *OIDCProvider) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.UserValidationRules != nil { + in, out := &in.UserValidationRules, &out.UserValidationRules + *out = make([]TokenUserValidationRule, len(*in)) + copy(*out, *in) + } return } @@ -6434,6 +6439,7 @@ func (in *TokenClaimValidationRule) DeepCopyInto(out *TokenClaimValidationRule) *out = new(TokenRequiredClaim) **out = **in } + out.Expression = in.Expression return } @@ -6468,6 +6474,22 @@ func (in *TokenConfig) DeepCopy() *TokenConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenExpressionRule) DeepCopyInto(out *TokenExpressionRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenExpressionRule. +func (in *TokenExpressionRule) DeepCopy() *TokenExpressionRule { + if in == nil { + return nil + } + out := new(TokenExpressionRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TokenIssuer) DeepCopyInto(out *TokenIssuer) { *out = *in @@ -6506,6 +6528,22 @@ func (in *TokenRequiredClaim) DeepCopy() *TokenRequiredClaim { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenUserValidationRule) DeepCopyInto(out *TokenUserValidationRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenUserValidationRule. +func (in *TokenUserValidationRule) DeepCopy() *TokenUserValidationRule { + if in == nil { + return nil + } + out := new(TokenUserValidationRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Update) DeepCopyInto(out *Update) { *out = *in diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml b/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml index 03b091ead..2d6642fde 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml @@ -31,6 +31,7 @@ authentications.config.openshift.io: FeatureGates: - ExternalOIDC - ExternalOIDCWithUIDAndExtraClaimMappings + - ExternalOIDCWithUpstreamParity FilenameOperatorName: config-operator FilenameOperatorOrdering: "01" FilenameRunLevel: "0000_10" diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go b/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go index be7d462a5..d613519b6 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go @@ -449,6 +449,7 @@ var map_OIDCProvider = map[string]string{ "oidcClients": "oidcClients is an optional field that configures how on-cluster, platform clients should request tokens from the identity provider. oidcClients must not exceed 20 entries and entries must have unique namespace/name pairs.", "claimMappings": "claimMappings is a required field that configures the rules to be used by the Kubernetes API server for translating claims in a JWT token, issued by the identity provider, to a cluster identity.", "claimValidationRules": "claimValidationRules is an optional field that configures the rules to be used by the Kubernetes API server for validating the claims in a JWT token issued by the identity provider.\n\nValidation rules are joined via an AND operation.", + "userValidationRules": "userValidationRules defines the set of rules used to validate claims in a user's token. Each rule is evaluated independently to determine whether the token subject is considered valid. Rules can either require specific claims and values to be present, or define CEL expressions that must evaluate to true for the token to be accepted. If the expression in a rule evaluates to false, the token is rejected. At least one rule must evaluate to true for the token to be considered valid. A maximum of 64 rules can be specified. This field is optional.\n\nSee https://kubernetes.io/docs/reference/using-api/cel/ for CEL syntax.", } func (OIDCProvider) SwaggerDoc() map[string]string { @@ -495,18 +496,30 @@ func (TokenClaimOrExpressionMapping) SwaggerDoc() map[string]string { } var map_TokenClaimValidationRule = map[string]string{ - "type": "type is an optional field that configures the type of the validation rule.\n\nAllowed values are 'RequiredClaim' and omitted (not provided or an empty string).\n\nWhen set to 'RequiredClaim', the Kubernetes API server will be configured to validate that the incoming JWT contains the required claim and that its value matches the required value.\n\nDefaults to 'RequiredClaim'.", - "requiredClaim": "requiredClaim is an optional field that configures the required claim and value that the Kubernetes API server will use to validate if an incoming JWT is valid for this identity provider.", + "": "TokenClaimValidationRule represents a validation rule based on token claims. If type is RequiredClaim, requiredClaim must be set. If type is Expression, expression must be set.", + "type": "type is an optional field that configures the type of the validation rule.\n\nAllowed values are \"RequiredClaim\" and \"Expression\".\n\nWhen set to 'RequiredClaim', the Kubernetes API server will be configured to validate that the incoming JWT contains the required claim and that its value matches the required value.\n\nWhen set to 'Expression', the Kubernetes API server will be configured to validate the incoming JWT against the configured CEL expression.", + "requiredClaim": "requiredClaim allows configuring a required claim name and its expected value. When type is RequiredClaim, this field is used by the Kubernetes API server to validate if an incoming JWT is valid for this identity provider.", + "expression": "expression configures a CEL expression that will be used by the Kubernetes API server to validate if an incoming JWT is valid for this identity provider. The CEL expression must return a boolean value where 'true' signals a valid state. Expression must be set when 'type' is 'Expression', and is forbidden otherwise.", } func (TokenClaimValidationRule) SwaggerDoc() map[string]string { return map_TokenClaimValidationRule } +var map_TokenExpressionRule = map[string]string{ + "expression": "expression is a CEL expression evaluated against token claims. The expression must be a non-empty string and no longer than 1024 characters. The expression must return a boolean value where 'true' signals a valid token and 'false' an invalid one. This field is required.", + "message": "message allows configuring a human-readable message that is logged by the Kubernetes API server when a token fails validation based on the CEL expression defined in 'Expression'. This field is optional. If provided, the message must be at least 1 character long and cannot exceed 256 characters. This message is logged and not returned to the caller.", +} + +func (TokenExpressionRule) SwaggerDoc() map[string]string { + return map_TokenExpressionRule +} + var map_TokenIssuer = map[string]string{ "issuerURL": "issuerURL is a required field that configures the URL used to issue tokens by the identity provider. The Kubernetes API server determines how authentication tokens should be handled by matching the 'iss' claim in the JWT to the issuerURL of configured identity providers.\n\nMust be at least 1 character and must not exceed 512 characters in length. Must be a valid URL that uses the 'https' scheme and does not contain a query, fragment or user.", "audiences": "audiences is a required field that configures the acceptable audiences the JWT token, issued by the identity provider, must be issued to. At least one of the entries must match the 'aud' claim in the JWT token.\n\naudiences must contain at least one entry and must not exceed ten entries.", "issuerCertificateAuthority": "issuerCertificateAuthority is an optional field that configures the certificate authority, used by the Kubernetes API server, to validate the connection to the identity provider when fetching discovery information.\n\nWhen not specified, the system trust is used.\n\nWhen specified, it must reference a ConfigMap in the openshift-config namespace containing the PEM-encoded CA certificates under the 'ca-bundle.crt' key in the data field of the ConfigMap.", + "discoveryURL": "discoveryURL is an optional field that, if specified, overrides the default discovery endpoint used to retrieve OIDC configuration metadata. By default, the discovery URL is derived from `issuerURL` as \"{url}/.well-known/openid-configuration\".\n\nThe discoveryURL must:\n - Be a valid absolute URL.\n - Use the HTTPS scheme.\n - Not contain query parameters, user info, or fragments.\n - Be different from the value of `url` (ignoring trailing slashes)", } func (TokenIssuer) SwaggerDoc() map[string]string { @@ -514,7 +527,7 @@ func (TokenIssuer) SwaggerDoc() map[string]string { } var map_TokenRequiredClaim = map[string]string{ - "claim": "claim is a required field that configures the name of the required claim. When taken from the JWT claims, claim must be a string value.\n\nclaim must not be an empty string (\"\").", + "claim": "When taken from the JWT claims, claim must be a string value.\n\nclaim must not be an empty string (\"\").", "requiredValue": "requiredValue is a required field that configures the value that 'claim' must have when taken from the incoming JWT claims. If the value in the JWT claims does not match, the token will be rejected for authentication.\n\nrequiredValue must not be an empty string (\"\").", } @@ -522,6 +535,16 @@ func (TokenRequiredClaim) SwaggerDoc() map[string]string { return map_TokenRequiredClaim } +var map_TokenUserValidationRule = map[string]string{ + "": "TokenUserValidationRule provides a CEL-based rule used to validate a token subject. Each rule contains a CEL expression that is evaluated against the token’s claims.", + "expression": "expression is a CEL expression that must evaluate to true for the token to be accepted. The expression is evaluated against the token's user information (e.g., username, groups). This field must be non-empty and may not exceed 1024 characters.", + "message": "message allows configuring a human-readable message that is logged by the Kubernetes API server when a token fails validation based on the CEL expression defined in 'Expression'. This field is optional. If provided, the message must be at least 1 character long and cannot exceed 256 characters. This message is logged and not returned to the caller.", +} + +func (TokenUserValidationRule) SwaggerDoc() map[string]string { + return map_TokenUserValidationRule +} + var map_UsernameClaimMapping = map[string]string{ "claim": "claim is a required field that configures the JWT token claim whose value is assigned to the cluster identity field associated with this mapping.\n\nclaim must not be an empty string (\"\") and must not exceed 256 characters.", "prefixPolicy": "prefixPolicy is an optional field that configures how a prefix should be applied to the value of the JWT claim specified in the 'claim' field.\n\nAllowed values are 'Prefix', 'NoPrefix', and omitted (not provided or an empty string).\n\nWhen set to 'Prefix', the value specified in the prefix field will be prepended to the value of the JWT claim. The prefix field must be set when prefixPolicy is 'Prefix'.\n\nWhen set to 'NoPrefix', no prefix will be prepended to the value of the JWT claim.\n\nWhen omitted, this means no opinion and the platform is left to choose any prefixes that are applied which is subject to change over time. Currently, the platform prepends `{issuerURL}#` to the value of the JWT claim when the claim is not 'email'. As an example, consider the following scenario:\n `prefix` is unset, `issuerURL` is set to `https://myoidc.tld`,\n the JWT claims include \"username\":\"userA\" and \"email\":\"userA@myoidc.tld\",\n and `claim` is set to:\n - \"username\": the mapped value will be \"https://myoidc.tld#userA\"\n - \"email\": the mapped value will be \"userA@myoidc.tld\"", @@ -878,7 +901,7 @@ var map_Update = map[string]string{ "architecture": "architecture is an optional field that indicates the desired value of the cluster architecture. In this context cluster architecture means either a single architecture or a multi architecture. architecture can only be set to Multi thereby only allowing updates from single to multi architecture. If architecture is set, image cannot be set and version must be set. Valid values are 'Multi' and empty.", "version": "version is a semantic version identifying the update version. version is required if architecture is specified. If both version and image are set, the version extracted from the referenced image must match the specified version.", "image": "image is a container image location that contains the update. image should be used when the desired version does not exist in availableUpdates or history. When image is set, architecture cannot be specified. If both version and image are set, the version extracted from the referenced image must match the specified version.", - "force": "force allows an administrator to update to an image that has failed verification or upgradeable checks that are designed to keep your cluster safe. Only use this if: * you are testing unsigned release images in short-lived test clusters or * you are working around a known bug in the cluster-version\n operator and you have verified the authenticity of the provided\n image yourself.\nThe provided image will run with full administrative access to the cluster. Do not use this flag with images that come from unknown or potentially malicious sources.", + "force": "force allows an administrator to update to an image that has failed verification or upgradeable checks that are designed to keep your cluster safe. Only use this if: * you are testing unsigned release images in short-lived test clusters or * you are working around a known bug in the cluster-version\n operator and you have verified the authenticity of the provided\n image yourself.\nThe provided image will run with full administrative access to the cluster. Do not use this flag with images that comes from unknown or potentially malicious sources.", } func (Update) SwaggerDoc() map[string]string { diff --git a/vendor/github.com/openshift/api/features.md b/vendor/github.com/openshift/api/features.md index df45c853f..04fa18d19 100644 --- a/vendor/github.com/openshift/api/features.md +++ b/vendor/github.com/openshift/api/features.md @@ -5,6 +5,7 @@ | MachineAPIOperatorDisableMachineHealthCheckController| | | | | | | | MultiArchInstallAzure| | | | | | | | ShortCertRotation| | | | | | | +| BootImageSkewEnforcement| | | Enabled | Enabled | | | | ClusterAPIMachineManagementVSphere| | | Enabled | Enabled | | | | Example2| | | Enabled | Enabled | | | | ExternalSnapshotMetadata| | | Enabled | Enabled | | | @@ -26,14 +27,11 @@ | AzureDedicatedHosts| | | Enabled | Enabled | Enabled | Enabled | | AzureDualStackInstall| | | Enabled | Enabled | Enabled | Enabled | | AzureMultiDisk| | | Enabled | Enabled | Enabled | Enabled | -| BootImageSkewEnforcement| | | Enabled | Enabled | Enabled | Enabled | | BootcNodeManagement| | | Enabled | Enabled | Enabled | Enabled | | CBORServingAndStorage| | | Enabled | Enabled | Enabled | Enabled | -| CRDCompatibilityRequirementOperator| | | Enabled | Enabled | Enabled | Enabled | | ClientsAllowCBOR| | | Enabled | Enabled | Enabled | Enabled | | ClientsPreferCBOR| | | Enabled | Enabled | Enabled | Enabled | | ClusterAPIInstallIBMCloud| | | Enabled | Enabled | Enabled | Enabled | -| ClusterAPIMachineManagement| | | Enabled | Enabled | Enabled | Enabled | | ClusterMonitoringConfig| | | Enabled | Enabled | Enabled | Enabled | | ClusterVersionOperatorConfiguration| | | Enabled | Enabled | Enabled | Enabled | | DNSNameResolver| | | Enabled | Enabled | Enabled | Enabled | @@ -43,6 +41,7 @@ | EtcdBackendQuota| | | Enabled | Enabled | Enabled | Enabled | | EventTTL| | | Enabled | Enabled | Enabled | Enabled | | Example| | | Enabled | Enabled | Enabled | Enabled | +| ExternalOIDCWithUpstreamParity| | | Enabled | Enabled | Enabled | Enabled | | GCPClusterHostedDNS| | | Enabled | Enabled | Enabled | Enabled | | GCPCustomAPIEndpoints| | | Enabled | Enabled | Enabled | Enabled | | GCPCustomAPIEndpointsInstall| | | Enabled | Enabled | Enabled | Enabled | diff --git a/vendor/github.com/openshift/api/features/features.go b/vendor/github.com/openshift/api/features/features.go index 910cabc5f..c3577afa6 100644 --- a/vendor/github.com/openshift/api/features/features.go +++ b/vendor/github.com/openshift/api/features/features.go @@ -366,7 +366,7 @@ var ( contactPerson("djoshy"). productScope(ocpSpecific). enhancementPR("https://github.com/openshift/enhancements/pull/1761"). - enableIn(configv1.DevPreviewNoUpgrade, configv1.TechPreviewNoUpgrade). + enableIn(configv1.DevPreviewNoUpgrade). mustRegister() FeatureGateBootcNodeManagement = newFeatureGate("BootcNodeManagement"). @@ -457,6 +457,14 @@ var ( enableIn(configv1.DevPreviewNoUpgrade, configv1.TechPreviewNoUpgrade, configv1.Default). mustRegister() + FeatureGateExternalOIDCWithUpstreamParity = newFeatureGate("ExternalOIDCWithUpstreamParity"). + reportProblemsToJiraComponent("authentication"). + contactPerson("saldawam"). + productScope(ocpSpecific). + enhancementPR("https://github.com/openshift/enhancements/pull/1763"). + enableIn(configv1.DevPreviewNoUpgrade, configv1.TechPreviewNoUpgrade). + mustRegister() + FeatureGateExample = newFeatureGate("Example"). reportProblemsToJiraComponent("cluster-config"). contactPerson("deads"). @@ -553,14 +561,6 @@ var ( enableIn(configv1.DevPreviewNoUpgrade, configv1.TechPreviewNoUpgrade). mustRegister() - FeatureGateClusterAPIMachineManagement = newFeatureGate("ClusterAPIMachineManagement"). - reportProblemsToJiraComponent("Cloud Compute / Cluster API Providers"). - contactPerson("ddonati"). - productScope(ocpSpecific). - enhancementPR("https://github.com/openshift/enhancements/pull/1465"). - enableIn(configv1.DevPreviewNoUpgrade, configv1.TechPreviewNoUpgrade). - mustRegister() - FeatureGateClusterAPIMachineManagementVSphere = newFeatureGate("ClusterAPIMachineManagementVSphere"). reportProblemsToJiraComponent("SPLAT"). contactPerson("jcpowermac"). @@ -908,12 +908,4 @@ var ( enhancementPR("https://github.com/openshift/enhancements/pull/1874"). enableIn(configv1.DevPreviewNoUpgrade, configv1.TechPreviewNoUpgrade). mustRegister() - - FeatureGateCRDCompatibilityRequirementOperator = newFeatureGate("CRDCompatibilityRequirementOperator"). - reportProblemsToJiraComponent("Cloud Compute / Cluster API Providers"). - contactPerson("ddonati"). - productScope(ocpSpecific). - enhancementPR("https://github.com/openshift/enhancements/pull/1845"). - enableIn(configv1.DevPreviewNoUpgrade, configv1.TechPreviewNoUpgrade). - mustRegister() ) diff --git a/vendor/github.com/openshift/api/machine/v1beta1/types_gcpprovider.go b/vendor/github.com/openshift/api/machine/v1beta1/types_gcpprovider.go index 9713a4e4a..72a31b5bd 100644 --- a/vendor/github.com/openshift/api/machine/v1beta1/types_gcpprovider.go +++ b/vendor/github.com/openshift/api/machine/v1beta1/types_gcpprovider.go @@ -25,14 +25,6 @@ const ( RestartPolicyNever GCPRestartPolicyType = "Never" ) -// GCPProvisioningModelType is a type representing acceptable values for ProvisioningModel field in GCPMachineProviderSpec -type GCPProvisioningModelType string - -const ( - // GCPSpotInstance enables the GCP instances as spot instances which provide significant cost savings but may be preempted by Google Cloud Platform when resources are needed elsewhere. - GCPSpotInstance GCPProvisioningModelType = "Spot" -) - // SecureBootPolicy represents the secure boot configuration for the GCP machine. type SecureBootPolicy string @@ -137,14 +129,6 @@ type GCPMachineProviderSpec struct { // preemptible indicates if created instance is preemptible. // +optional Preemptible bool `json:"preemptible,omitempty"` - // provisioningModel is an optional field that determines the provisioning model for the GCP machine instance. - // Valid values are "Spot" and omitted. - // When set to Spot, the instance runs as a Google Cloud Spot instance which provides significant cost savings but may be preempted by Google Cloud Platform when resources are needed elsewhere. - // When omitted, the machine will be provisioned as a standard on-demand instance. - // This field cannot be used together with the preemptible field. - // +optional - // +kubebuilder:validation:Enum=Spot - ProvisioningModel *GCPProvisioningModelType `json:"provisioningModel,omitempty"` // onHostMaintenance determines the behavior when a maintenance event occurs that might cause the instance to reboot. // This is required to be set to "Terminate" if you want to provision machine with attached GPUs. // Otherwise, allowed values are "Migrate" and "Terminate". diff --git a/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.deepcopy.go index 554fc19b9..5aa4f90a4 100644 --- a/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.deepcopy.go @@ -762,11 +762,6 @@ func (in *GCPMachineProviderSpec) DeepCopyInto(out *GCPMachineProviderSpec) { *out = make([]GCPGPUConfig, len(*in)) copy(*out, *in) } - if in.ProvisioningModel != nil { - in, out := &in.ProvisioningModel, &out.ProvisioningModel - *out = new(GCPProvisioningModelType) - **out = **in - } out.ShieldedInstanceConfig = in.ShieldedInstanceConfig if in.ResourceManagerTags != nil { in, out := &in.ResourceManagerTags, &out.ResourceManagerTags diff --git a/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.swagger_doc_generated.go b/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.swagger_doc_generated.go index 7b74d37d0..4a1b969a8 100644 --- a/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.swagger_doc_generated.go +++ b/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.swagger_doc_generated.go @@ -452,7 +452,6 @@ var map_GCPMachineProviderSpec = map[string]string{ "projectID": "projectID is the project in which the GCP machine provider will create the VM.", "gpus": "gpus is a list of GPUs to be attached to the VM.", "preemptible": "preemptible indicates if created instance is preemptible.", - "provisioningModel": "provisioningModel is an optional field that determines the provisioning model for the GCP machine instance. Valid values are \"Spot\" and omitted. When set to Spot, the instance runs as a Google Cloud Spot instance which provides significant cost savings but may be preempted by Google Cloud Platform when resources are needed elsewhere. When omitted, the machine will be provisioned as a standard on-demand instance. This field cannot be used together with the preemptible field.", "onHostMaintenance": "onHostMaintenance determines the behavior when a maintenance event occurs that might cause the instance to reboot. This is required to be set to \"Terminate\" if you want to provision machine with attached GPUs. Otherwise, allowed values are \"Migrate\" and \"Terminate\". If omitted, the platform chooses a default, which is subject to change over time, currently that default is \"Migrate\".", "restartPolicy": "restartPolicy determines the behavior when an instance crashes or the underlying infrastructure provider stops the instance as part of a maintenance event (default \"Always\"). Cannot be \"Always\" with preemptible instances. Otherwise, allowed values are \"Always\" and \"Never\". If omitted, the platform chooses a default, which is subject to change over time, currently that default is \"Always\". RestartPolicy represents AutomaticRestart in GCP compute api", "shieldedInstanceConfig": "shieldedInstanceConfig is the Shielded VM configuration for the VM", diff --git a/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go b/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go index 53c71aabb..279990448 100644 --- a/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go +++ b/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go @@ -81,6 +81,7 @@ const ( CinderCSIDriver CSIDriverName = "cinder.csi.openstack.org" VSphereCSIDriver CSIDriverName = "csi.vsphere.vmware.com" ManilaCSIDriver CSIDriverName = "manila.csi.openstack.org" + OvirtCSIDriver CSIDriverName = "csi.ovirt.org" KubevirtCSIDriver CSIDriverName = "csi.kubevirt.io" SharedResourcesCSIDriver CSIDriverName = "csi.sharedresource.openshift.io" AlibabaDiskCSIDriver CSIDriverName = "diskplugin.csi.alibabacloud.com" diff --git a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-CustomNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-CustomNoUpgrade.crd.yaml index 45486c270..8e2ab77f1 100644 --- a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-CustomNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-CustomNoUpgrade.crd.yaml @@ -55,6 +55,7 @@ spec: - cinder.csi.openstack.org - csi.vsphere.vmware.com - manila.csi.openstack.org + - csi.ovirt.org - csi.kubevirt.io - csi.sharedresource.openshift.io - diskplugin.csi.alibabacloud.com diff --git a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-Default.crd.yaml b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-Default.crd.yaml index 1b64e9e9a..daf1f8abd 100644 --- a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-Default.crd.yaml +++ b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-Default.crd.yaml @@ -55,6 +55,7 @@ spec: - cinder.csi.openstack.org - csi.vsphere.vmware.com - manila.csi.openstack.org + - csi.ovirt.org - csi.kubevirt.io - csi.sharedresource.openshift.io - diskplugin.csi.alibabacloud.com diff --git a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-DevPreviewNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-DevPreviewNoUpgrade.crd.yaml index 7029b1bde..e8766002d 100644 --- a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-DevPreviewNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-DevPreviewNoUpgrade.crd.yaml @@ -55,6 +55,7 @@ spec: - cinder.csi.openstack.org - csi.vsphere.vmware.com - manila.csi.openstack.org + - csi.ovirt.org - csi.kubevirt.io - csi.sharedresource.openshift.io - diskplugin.csi.alibabacloud.com diff --git a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-TechPreviewNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-TechPreviewNoUpgrade.crd.yaml index 04052c180..98f87a356 100644 --- a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-TechPreviewNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_50_csi-driver_01_clustercsidrivers-TechPreviewNoUpgrade.crd.yaml @@ -55,6 +55,7 @@ spec: - cinder.csi.openstack.org - csi.vsphere.vmware.com - manila.csi.openstack.org + - csi.ovirt.org - csi.kubevirt.io - csi.sharedresource.openshift.io - diskplugin.csi.alibabacloud.com diff --git a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfigurations-TechPreviewNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfigurations-TechPreviewNoUpgrade.crd.yaml index 0cc415a58..14a864201 100644 --- a/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfigurations-TechPreviewNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/operator/v1/zz_generated.crd-manifests/0000_80_machine-config_01_machineconfigurations-TechPreviewNoUpgrade.crd.yaml @@ -46,98 +46,6 @@ spec: description: spec is the specification of the desired behavior of the Machine Config Operator properties: - bootImageSkewEnforcement: - description: |- - bootImageSkewEnforcement allows an admin to configure how boot image version skew is - enforced on the cluster. - When omitted, this will default to Automatic for clusters that support automatic boot image updates. - For clusters that do not support automatic boot image updates, cluster upgrades will be disabled until - a skew enforcement mode has been specified. - When version skew is being enforced, cluster upgrades will be disabled until the version skew is deemed - acceptable for the current release payload. - properties: - manual: - description: |- - manual describes the current boot image of the cluster. - This should be set to the oldest boot image used amongst all machine resources in the cluster. - This must include either the RHCOS version of the boot image or the OCP release version which shipped with that - RHCOS boot image. - Required when mode is set to "Manual" and forbidden otherwise. - properties: - mode: - description: |- - mode is used to configure which boot image field is defined in Manual mode. - Valid values are OCPVersion and RHCOSVersion. - OCPVersion means that the cluster admin is expected to set the OCP version associated with the last boot image update - in the OCPVersion field. - RHCOSVersion means that the cluster admin is expected to set the RHCOS version associated with the last boot image update - in the RHCOSVersion field. - This field is required. - enum: - - OCPVersion - - RHCOSVersion - type: string - ocpVersion: - description: |- - ocpVersion provides a string which represents the OCP version of the boot image. - This field must match the OCP semver compatible format of x.y.z. This field must be between - 5 and 10 characters long. - Required when mode is set to "OCPVersion" and forbidden otherwise. - maxLength: 10 - minLength: 5 - type: string - x-kubernetes-validations: - - message: ocpVersion must match the OCP semver compatible - format of x.y.z - rule: self.matches('^[0-9]+\\.[0-9]+\\.[0-9]+$') - rhcosVersion: - description: |- - rhcosVersion provides a string which represents the RHCOS version of the boot image - This field must match rhcosVersion formatting of [major].[minor].[datestamp(YYYYMMDD)]-[buildnumber] or the legacy - format of [major].[minor].[timestamp(YYYYMMDDHHmm)]-[buildnumber]. This field must be between - 14 and 21 characters long. - Required when mode is set to "RHCOSVersion" and forbidden otherwise. - maxLength: 21 - minLength: 14 - type: string - x-kubernetes-validations: - - message: rhcosVersion must match format [major].[minor].[datestamp(YYYYMMDD)]-[buildnumber] - or must match legacy format [major].[minor].[timestamp(YYYYMMDDHHmm)]-[buildnumber] - rule: self.matches('^[0-9]+\\.[0-9]+\\.([0-9]{8}|[0-9]{12})-[0-9]+$') - required: - - mode - type: object - x-kubernetes-validations: - - message: ocpVersion is required when mode is OCPVersion, and - forbidden otherwise - rule: 'has(self.mode) && (self.mode ==''OCPVersion'') ? has(self.ocpVersion) - : !has(self.ocpVersion)' - - message: rhcosVersion is required when mode is RHCOSVersion, - and forbidden otherwise - rule: 'has(self.mode) && (self.mode ==''RHCOSVersion'') ? has(self.rhcosVersion) - : !has(self.rhcosVersion)' - mode: - description: |- - mode determines the underlying behavior of skew enforcement mechanism. - Valid values are Manual and None. - Manual means that the cluster admin is expected to perform manual boot image updates and store the OCP - & RHCOS version associated with the last boot image update in the manual field. - In Manual mode, the MCO will prevent upgrades when the boot image skew exceeds the - skew limit described by the release image. - None means that the MCO will no longer monitor the boot image skew. This may affect - the cluster's ability to scale. - This field is required. - enum: - - Manual - - None - type: string - required: - - mode - type: object - x-kubernetes-validations: - - message: manual is required when mode is Manual, and forbidden otherwise - rule: 'has(self.mode) && (self.mode ==''Manual'') ? has(self.manual) - : !has(self.manual)' failedRevisionLimit: description: |- failedRevisionLimit is the number of failed static pod installer revisions to keep on disk and in the api @@ -782,140 +690,6 @@ spec: description: status is the most recently observed status of the Machine Config Operator properties: - bootImageSkewEnforcementStatus: - description: |- - bootImageSkewEnforcementStatus reflects what the latest cluster-validated boot image skew enforcement - configuration is and will be used by Machine Config Controller while performing boot image skew enforcement. - When omitted, the MCO has no knowledge of how to enforce boot image skew. When the MCO does not know how - boot image skew should be enforced, cluster upgrades will be blocked until it can either automatically - determine skew enforcement or there is an explicit skew enforcement configuration provided in the - spec.bootImageSkewEnforcement field. - properties: - automatic: - description: |- - automatic describes the current boot image of the cluster. - This will be populated by the MCO when performing boot image updates. This value will be compared against - the cluster's skew limit to determine skew compliance. - Required when mode is set to "Automatic" and forbidden otherwise. - minProperties: 1 - properties: - ocpVersion: - description: |- - ocpVersion provides a string which represents the OCP version of the boot image. - This field must match the OCP semver compatible format of x.y.z. This field must be between - 5 and 10 characters long. - maxLength: 10 - minLength: 5 - type: string - x-kubernetes-validations: - - message: ocpVersion must match the OCP semver compatible - format of x.y.z - rule: self.matches('^[0-9]+\\.[0-9]+\\.[0-9]+$') - rhcosVersion: - description: |- - rhcosVersion provides a string which represents the RHCOS version of the boot image - This field must match rhcosVersion formatting of [major].[minor].[datestamp(YYYYMMDD)]-[buildnumber] or the legacy - format of [major].[minor].[timestamp(YYYYMMDDHHmm)]-[buildnumber]. This field must be between - 14 and 21 characters long. - maxLength: 21 - minLength: 14 - type: string - x-kubernetes-validations: - - message: rhcosVersion must match format [major].[minor].[datestamp(YYYYMMDD)]-[buildnumber] - or must match legacy format [major].[minor].[timestamp(YYYYMMDDHHmm)]-[buildnumber] - rule: self.matches('^[0-9]+\\.[0-9]+\\.([0-9]{8}|[0-9]{12})-[0-9]+$') - type: object - x-kubernetes-validations: - - message: at least one of ocpVersion or rhcosVersion is required - rule: has(self.ocpVersion) || has(self.rhcosVersion) - manual: - description: |- - manual describes the current boot image of the cluster. - This will be populated by the MCO using the values provided in the spec.bootImageSkewEnforcement.manual field. - This value will be compared against the cluster's skew limit to determine skew compliance. - Required when mode is set to "Manual" and forbidden otherwise. - properties: - mode: - description: |- - mode is used to configure which boot image field is defined in Manual mode. - Valid values are OCPVersion and RHCOSVersion. - OCPVersion means that the cluster admin is expected to set the OCP version associated with the last boot image update - in the OCPVersion field. - RHCOSVersion means that the cluster admin is expected to set the RHCOS version associated with the last boot image update - in the RHCOSVersion field. - This field is required. - enum: - - OCPVersion - - RHCOSVersion - type: string - ocpVersion: - description: |- - ocpVersion provides a string which represents the OCP version of the boot image. - This field must match the OCP semver compatible format of x.y.z. This field must be between - 5 and 10 characters long. - Required when mode is set to "OCPVersion" and forbidden otherwise. - maxLength: 10 - minLength: 5 - type: string - x-kubernetes-validations: - - message: ocpVersion must match the OCP semver compatible - format of x.y.z - rule: self.matches('^[0-9]+\\.[0-9]+\\.[0-9]+$') - rhcosVersion: - description: |- - rhcosVersion provides a string which represents the RHCOS version of the boot image - This field must match rhcosVersion formatting of [major].[minor].[datestamp(YYYYMMDD)]-[buildnumber] or the legacy - format of [major].[minor].[timestamp(YYYYMMDDHHmm)]-[buildnumber]. This field must be between - 14 and 21 characters long. - Required when mode is set to "RHCOSVersion" and forbidden otherwise. - maxLength: 21 - minLength: 14 - type: string - x-kubernetes-validations: - - message: rhcosVersion must match format [major].[minor].[datestamp(YYYYMMDD)]-[buildnumber] - or must match legacy format [major].[minor].[timestamp(YYYYMMDDHHmm)]-[buildnumber] - rule: self.matches('^[0-9]+\\.[0-9]+\\.([0-9]{8}|[0-9]{12})-[0-9]+$') - required: - - mode - type: object - x-kubernetes-validations: - - message: ocpVersion is required when mode is OCPVersion, and - forbidden otherwise - rule: 'has(self.mode) && (self.mode ==''OCPVersion'') ? has(self.ocpVersion) - : !has(self.ocpVersion)' - - message: rhcosVersion is required when mode is RHCOSVersion, - and forbidden otherwise - rule: 'has(self.mode) && (self.mode ==''RHCOSVersion'') ? has(self.rhcosVersion) - : !has(self.rhcosVersion)' - mode: - description: |- - mode determines the underlying behavior of skew enforcement mechanism. - Valid values are Automatic, Manual and None. - Automatic means that the MCO will perform boot image updates and store the - OCP & RHCOS version associated with the last boot image update in the automatic field. - Manual means that the cluster admin is expected to perform manual boot image updates and store the OCP - & RHCOS version associated with the last boot image update in the manual field. - In Automatic and Manual mode, the MCO will prevent upgrades when the boot image skew exceeds the - skew limit described by the release image. - None means that the MCO will no longer monitor the boot image skew. This may affect - the cluster's ability to scale. - This field is required. - enum: - - Automatic - - Manual - - None - type: string - required: - - mode - type: object - x-kubernetes-validations: - - message: automatic is required when mode is Automatic, and forbidden - otherwise - rule: 'has(self.mode) && (self.mode == ''Automatic'') ? has(self.automatic) - : !has(self.automatic)' - - message: manual is required when mode is Manual, and forbidden otherwise - rule: 'has(self.mode) && (self.mode == ''Manual'') ? has(self.manual) - : !has(self.manual)' conditions: description: conditions is a list of conditions and their status items: @@ -1518,25 +1292,6 @@ spec: required: - spec type: object - x-kubernetes-validations: - - message: when skew enforcement is in Automatic mode, a boot image configuration - is required - rule: 'self.?status.bootImageSkewEnforcementStatus.mode.orValue("") == ''Automatic'' - ? self.?spec.managedBootImages.hasValue() || self.?status.managedBootImagesStatus.hasValue() - : true' - - message: when skew enforcement is in Automatic mode, managedBootImages must - contain a MachineManager opting in all MachineAPI MachineSets - rule: 'self.?status.bootImageSkewEnforcementStatus.mode.orValue("") == ''Automatic'' - ? !(self.?spec.managedBootImages.machineManagers.hasValue()) || self.spec.managedBootImages.machineManagers.exists(m, - m.selection.mode == ''All'' && m.resource == ''machinesets'' && m.apiGroup - == ''machine.openshift.io'') : true' - - message: when skew enforcement is in Automatic mode, managedBootImagesStatus - must contain a MachineManager opting in all MachineAPI MachineSets - rule: 'self.?status.bootImageSkewEnforcementStatus.mode.orValue("") == ''Automatic'' - ? !(self.?status.managedBootImagesStatus.machineManagers.hasValue()) || - self.status.managedBootImagesStatus.machineManagers.exists(m, m.selection.mode - == ''All'' && m.resource == ''machinesets'' && m.apiGroup == ''machine.openshift.io''): - true' served: true storage: true subresources: diff --git a/vendor/modules.txt b/vendor/modules.txt index bfcb979b7..71b7f39ef 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -173,7 +173,7 @@ github.com/modern-go/reflect2 # github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 ## explicit github.com/munnerz/goautoneg -# github.com/openshift/api v0.0.0-20251106190826-ebe535b08719 +# github.com/openshift/api v0.0.0-20251106190826-ebe535b08719 => github.com/ShazaAldawamneh/api v0.0.0-20251105172922-8847fb59bcff ## explicit; go 1.24.0 github.com/openshift/api github.com/openshift/api/annotations @@ -1552,3 +1552,4 @@ sigs.k8s.io/structured-merge-diff/v6/value # sigs.k8s.io/yaml v1.6.0 ## explicit; go 1.22 sigs.k8s.io/yaml +# github.com/openshift/api => github.com/ShazaAldawamneh/api v0.0.0-20251105172922-8847fb59bcff From 0a90751d8c17ee9e61695945b45fd1b56e18b91c Mon Sep 17 00:00:00 2001 From: Shaza Aldawamneh Date: Tue, 2 Dec 2025 12:16:00 +0100 Subject: [PATCH 2/3] feature gating all new fields Signed-off-by: Shaza Aldawamneh --- .../externaloidc/externaloidc_controller.go | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/pkg/controllers/externaloidc/externaloidc_controller.go b/pkg/controllers/externaloidc/externaloidc_controller.go index e80204f9c..7a1ced154 100644 --- a/pkg/controllers/externaloidc/externaloidc_controller.go +++ b/pkg/controllers/externaloidc/externaloidc_controller.go @@ -178,7 +178,7 @@ func (c *externalOIDCController) generateAuthConfig(auth configv1.Authentication func generateJWTForProvider(provider configv1.OIDCProvider, configMapLister corev1listers.ConfigMapLister, featureGates featuregates.FeatureGate, serviceAccountIssuer string) (apiserverv1beta1.JWTAuthenticator, error) { out := apiserverv1beta1.JWTAuthenticator{} - issuer, err := generateIssuer(provider.Issuer, configMapLister, serviceAccountIssuer) + issuer, err := generateIssuer(provider.Issuer, featureGates, configMapLister, serviceAccountIssuer) if err != nil { return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating issuer for provider %q: %v", provider.Name, err) } @@ -188,16 +188,18 @@ func generateJWTForProvider(provider configv1.OIDCProvider, configMapLister core return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating claimMappings for provider %q: %v", provider.Name, err) } - claimValidationRules, err := generateClaimValidationRules(provider.ClaimValidationRules...) + claimValidationRules, err := generateClaimValidationRules(featureGates, provider.ClaimValidationRules...) if err != nil { return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating claimValidationRules for provider %q: %v", provider.Name, err) } - userValidationRules, err := generateUserValidationRules(provider.UserValidationRules) - if err != nil { - return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating userValidationRules for provider %q: %v", provider.Name, err) + var userValidationRules []apiserverv1beta1.UserValidationRule + if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + userValidationRules, err = generateUserValidationRules(featureGates, provider.UserValidationRules) + if err != nil { + return apiserverv1beta1.JWTAuthenticator{}, fmt.Errorf("generating userValidationRules for provider %q: %v", provider.Name, err) + } } - out.Issuer = issuer out.ClaimMappings = claimMappings out.ClaimValidationRules = claimValidationRules @@ -206,7 +208,7 @@ func generateJWTForProvider(provider configv1.OIDCProvider, configMapLister core return out, nil } -func generateIssuer(issuer configv1.TokenIssuer, configMapLister corev1listers.ConfigMapLister, serviceAccountIssuer string) (apiserverv1beta1.Issuer, error) { +func generateIssuer(issuer configv1.TokenIssuer, featureGates featuregates.FeatureGate, configMapLister corev1listers.ConfigMapLister, serviceAccountIssuer string) (apiserverv1beta1.Issuer, error) { out := apiserverv1beta1.Issuer{} if len(serviceAccountIssuer) > 0 { @@ -221,9 +223,10 @@ func generateIssuer(issuer configv1.TokenIssuer, configMapLister corev1listers.C for _, audience := range issuer.Audiences { out.Audiences = append(out.Audiences, string(audience)) } - - if issuer.DiscoveryURL != "" { - out.DiscoveryURL = &issuer.DiscoveryURL + if featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + if issuer.DiscoveryURL != "" { + out.DiscoveryURL = &issuer.DiscoveryURL + } } if len(issuer.CertificateAuthority.Name) > 0 { @@ -404,11 +407,11 @@ func generateExtraMapping(extraMapping configv1.ExtraMapping) (apiserverv1beta1. return out, nil } -func generateClaimValidationRules(claimValidationRules ...configv1.TokenClaimValidationRule) ([]apiserverv1beta1.ClaimValidationRule, error) { +func generateClaimValidationRules(featureGates featuregates.FeatureGate, claimValidationRules ...configv1.TokenClaimValidationRule) ([]apiserverv1beta1.ClaimValidationRule, error) { out := []apiserverv1beta1.ClaimValidationRule{} errs := []error{} for _, claimValidationRule := range claimValidationRules { - rule, err := generateClaimValidationRule(claimValidationRule) + rule, err := generateClaimValidationRule(claimValidationRule, featureGates) if err != nil { errs = append(errs, fmt.Errorf("generating claimValidationRule: %v", err)) continue @@ -424,7 +427,7 @@ func generateClaimValidationRules(claimValidationRules ...configv1.TokenClaimVal return out, nil } -func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidationRule) (apiserverv1beta1.ClaimValidationRule, error) { +func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidationRule, featureGates featuregates.FeatureGate) (apiserverv1beta1.ClaimValidationRule, error) { out := apiserverv1beta1.ClaimValidationRule{} // Currently, the authentications.config.openshift.io CRD only allows setting a claim and required value for the @@ -439,7 +442,12 @@ func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidati out.Claim = claimValidationRule.RequiredClaim.Claim out.RequiredValue = claimValidationRule.RequiredClaim.RequiredValue case configv1.TokenValidationRuleExpression: - if claimValidationRule.Expression.Expression == "" { + if !featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + // skip CEL expression handling if the feature is disabled + return apiserverv1beta1.ClaimValidationRule{}, fmt.Errorf( + "TokenValidationRuleExpression is not enabled without the feature gate") + } + if len(claimValidationRule.Expression.Expression) == 0 { return apiserverv1beta1.ClaimValidationRule{}, fmt.Errorf("claimValidationRule.type is %s and expression is not set", configv1.TokenValidationRuleExpression) } @@ -460,24 +468,40 @@ func generateClaimValidationRule(claimValidationRule configv1.TokenClaimValidati return out, nil } -func generateUserValidationRules(rules []configv1.TokenUserValidationRule) ([]apiserverv1beta1.UserValidationRule, error) { +func generateUserValidationRule(rule configv1.TokenUserValidationRule, featureGates featuregates.FeatureGate) (apiserverv1beta1.UserValidationRule, error) { + if len(rule.Expression) == 0 { + return apiserverv1beta1.UserValidationRule{}, fmt.Errorf("userValidationRule expression must be non-empty") + } + + // Optional: only enable user validation rules if a feature gate is active + if !featureGates.Enabled(features.FeatureGateExternalOIDCWithUpstreamParity) { + return apiserverv1beta1.UserValidationRule{}, fmt.Errorf( + "userValidationRule cannot be used without the feature gate") + } + + return apiserverv1beta1.UserValidationRule{ + Expression: rule.Expression, + Message: rule.Message, + }, nil +} + +func generateUserValidationRules(featureGates featuregates.FeatureGate, rules []configv1.TokenUserValidationRule) ([]apiserverv1beta1.UserValidationRule, error) { out := []apiserverv1beta1.UserValidationRule{} errs := []error{} + for _, r := range rules { - // Validate the expression is non-empty - if len(r.Expression) == 0 { - errs = append(errs, fmt.Errorf("userValidationRule expression must be non-empty")) + uvr, err := generateUserValidationRule(r, featureGates) + if err != nil { + errs = append(errs, fmt.Errorf("generating userValidationRule: %v", err)) continue } - uvr := apiserverv1beta1.UserValidationRule{ - Expression: r.Expression, - Message: r.Message, - } out = append(out, uvr) } + if len(errs) > 0 { return nil, errors.Join(errs...) } + return out, nil } From 07257c73f875516ec90920ef09b60ae4ae3df90c Mon Sep 17 00:00:00 2001 From: Shaza Aldawamneh Date: Mon, 8 Dec 2025 15:39:33 +0100 Subject: [PATCH 3/3] featuregating tests Signed-off-by: Shaza Aldawamneh --- .../externaloidc_controller_test.go | 52 ++++++------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/pkg/controllers/externaloidc/externaloidc_controller_test.go b/pkg/controllers/externaloidc/externaloidc_controller_test.go index 1f16960ee..bdcec91bd 100644 --- a/pkg/controllers/externaloidc/externaloidc_controller_test.go +++ b/pkg/controllers/externaloidc/externaloidc_controller_test.go @@ -63,7 +63,6 @@ var ( Issuer: configv1.TokenIssuer{ CertificateAuthority: configv1.ConfigMapNameReference{Name: "oidc-ca-bundle"}, Audiences: []configv1.TokenAudience{"my-test-aud", "another-aud"}, - DiscoveryURL: "https://example.com/.well-known/openid-configuration", }, OIDCClients: []configv1.OIDCClientConfig{ { @@ -101,19 +100,13 @@ var ( }, }, { - Type: configv1.TokenValidationRuleExpression, - Expression: configv1.TokenExpressionRule{ - Expression: "claims.email.endsWith('@example.com')", - Message: "email domain must be example.com", + Type: configv1.TokenValidationRuleRequiredClaim, + RequiredClaim: &configv1.TokenRequiredClaim{ + Claim: "email", + RequiredValue: "test-email", }, }, }, - UserValidationRules: []configv1.TokenUserValidationRule{ - { - Expression: "user.groups.exists(g, g == 'system:authenticated')", - Message: "user must be authenticated", - }, - }, }, }, }) @@ -129,7 +122,6 @@ var ( Audiences: []string{"my-test-aud", "another-aud"}, CertificateAuthority: testCertData, AudienceMatchPolicy: apiserverv1beta1.AudienceMatchPolicyMatchAny, - DiscoveryURL: ptr.To("https://example.com/.well-known/openid-configuration"), }, ClaimMappings: apiserverv1beta1.ClaimMappings{ Username: apiserverv1beta1.PrefixedClaimOrExpression{ @@ -147,22 +139,17 @@ var ( RequiredValue: "test-username", }, { - Expression: "claims.email.endsWith('@example.com')", - Message: "email domain must be example.com", - }, - }, - UserValidationRules: []apiserverv1beta1.UserValidationRule{ - { - Expression: "user.groups.exists(g, g == 'system:authenticated')", - Message: "user must be authenticated", + Claim: "email", + RequiredValue: "test-email", }, }, }, }, } - baseAuthConfigJSON = fmt.Sprintf(`{"kind":"%s","apiVersion":"apiserver.config.k8s.io/v1beta1","jwt":[{"issuer":{"url":"$URL","certificateAuthority":"%s","audiences":["my-test-aud","another-aud"],"audienceMatchPolicy":"MatchAny"},"claimValidationRules":[{"claim":"username","requiredValue":"test-username"},{"claim":"email","requiredValue":"test-email"},{"expression":"claims.email.endsWith('@example.com')","message":"email domain must be example.com"}],"userValidationRules":[{"expression":"user.groups.exists(g, g == 'system:authenticated')","message":"user must be authenticated"}],"claimMappings":{"username":{"claim":"username","prefix":"oidc-user:"},"groups":{"claim":"groups","prefix":"oidc-group:"},"uid":{}}}]}`, kindAuthenticationConfiguration, strings.ReplaceAll(testCertData, "\n", "\\n")) - baseAuthConfigCM = corev1.ConfigMap{ + baseAuthConfigJSON = fmt.Sprintf(`{"kind":"%s","apiVersion":"apiserver.config.k8s.io/v1beta1","jwt":[{"issuer":{"url":"$URL","certificateAuthority":"%s","audiences":["my-test-aud","another-aud"],"audienceMatchPolicy":"MatchAny"},"claimValidationRules":[{"claim":"username","requiredValue":"test-username"},{"claim":"email","requiredValue":"test-email"}],"claimMappings":{"username":{"claim":"username","prefix":"oidc-user:"},"groups":{"claim":"groups","prefix":"oidc-group:"},"uid":{}}}]}`, kindAuthenticationConfiguration, strings.ReplaceAll(testCertData, "\n", "\\n")) + + baseAuthConfigCM = corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: targetAuthConfigCMName, Namespace: managedNamespace, @@ -706,7 +693,7 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { copy.Spec.OIDCProviders[i].ClaimValidationRules = append( copy.Spec.OIDCProviders[i].ClaimValidationRules, configv1.TokenClaimValidationRule{ - Type: configv1.TokenValidationRuleRequiredClaim, // updated constant + Type: configv1.TokenValidationRuleRequiredClaim, RequiredClaim: nil, }, ) @@ -717,7 +704,8 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { featureGates: featuregates.NewFeatureGate( []configv1.FeatureGateName{}, []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithUpstreamParity}, + features.FeatureGateExternalOIDCWithAdditionalClaimMappings, + }, ), }, { @@ -1080,10 +1068,8 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { }), expectError: true, featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - }, ), }, { @@ -1095,10 +1081,8 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { }), expectError: true, featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - }, ), }, { @@ -1116,10 +1100,8 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { }), expectError: true, featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithUpstreamParity, - }, ), }, { @@ -1134,10 +1116,8 @@ func TestExternalOIDCController_generateAuthConfig(t *testing.T) { }), expectError: true, featureGates: featuregates.NewFeatureGate( + []configv1.FeatureGateName{features.FeatureGateExternalOIDCWithUpstreamParity}, []configv1.FeatureGateName{}, - []configv1.FeatureGateName{ - features.FeatureGateExternalOIDCWithAdditionalClaimMappings, - }, ), }, } {