From b1446d9adde7da625f25beaf7d3f86b553d35c84 Mon Sep 17 00:00:00 2001 From: Shaza Aldawamneh Date: Tue, 7 Jul 2026 14:39:10 +0200 Subject: [PATCH] feat: Promote ExternalOIDCWithUpstreamParity to Default feature set Promote the ExternalOIDCWithUpstreamParity feature gate from TechPreviewNoUpgrade to the Default feature set, enabling upstream parity authentication configuration by default. This change aligns with the openshift/api promotion in PR #2915 and allows the feature to be available in the default feature set once the API changes are merged. Signed-off-by: Shaza Aldawamneh --- .../featureGate-Hypershift-Default.yaml | 6 +- .../featureGate-SelfManagedHA-Default.yaml | 6 +- .../featureGate-Hypershift-Default.yaml | 6 +- .../featureGate-SelfManagedHA-Default.yaml | 6 +- ...hostedclusters-Hypershift-Default.crd.yaml | 144 +++++++++++++++++- ...dcontrolplanes-Hypershift-Default.crd.yaml | 144 +++++++++++++++++- .../hostedcontrolplane/v2/kas/auth_test.go | 1 + .../featuregates/featuregates.go | 2 +- 8 files changed, 294 insertions(+), 21 deletions(-) diff --git a/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-Default.yaml b/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-Default.yaml index a0871fbcc733..57522ab0257a 100644 --- a/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-Default.yaml +++ b/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-Default.yaml @@ -18,9 +18,6 @@ { "name": "AROHCPManagedIdentities" }, - { - "name": "ExternalOIDCWithUpstreamParity" - }, { "name": "OpenStack" }, @@ -49,6 +46,9 @@ { "name": "ExternalOIDCWithUIDAndExtraClaimMappings" }, + { + "name": "ExternalOIDCWithUpstreamParity" + }, { "name": "HyperShiftOnlyDynamicResourceAllocation" }, diff --git a/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-Default.yaml b/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-Default.yaml index 1df673238659..3973e96384e4 100644 --- a/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-Default.yaml +++ b/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-Default.yaml @@ -15,9 +15,6 @@ "featureGates": [ { "disabled": [ - { - "name": "ExternalOIDCWithUpstreamParity" - }, { "name": "AROHCPManagedIdentities" }, @@ -49,6 +46,9 @@ { "name": "ExternalOIDCWithUIDAndExtraClaimMappings" }, + { + "name": "ExternalOIDCWithUpstreamParity" + }, { "name": "DynamicResourceAllocation" }, diff --git a/cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-Hypershift-Default.yaml b/cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-Hypershift-Default.yaml index a0871fbcc733..57522ab0257a 100644 --- a/cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-Hypershift-Default.yaml +++ b/cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-Hypershift-Default.yaml @@ -18,9 +18,6 @@ { "name": "AROHCPManagedIdentities" }, - { - "name": "ExternalOIDCWithUpstreamParity" - }, { "name": "OpenStack" }, @@ -49,6 +46,9 @@ { "name": "ExternalOIDCWithUIDAndExtraClaimMappings" }, + { + "name": "ExternalOIDCWithUpstreamParity" + }, { "name": "HyperShiftOnlyDynamicResourceAllocation" }, diff --git a/cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-SelfManagedHA-Default.yaml b/cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-SelfManagedHA-Default.yaml index 1df673238659..3973e96384e4 100644 --- a/cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-SelfManagedHA-Default.yaml +++ b/cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-SelfManagedHA-Default.yaml @@ -15,9 +15,6 @@ "featureGates": [ { "disabled": [ - { - "name": "ExternalOIDCWithUpstreamParity" - }, { "name": "AROHCPManagedIdentities" }, @@ -49,6 +46,9 @@ { "name": "ExternalOIDCWithUIDAndExtraClaimMappings" }, + { + "name": "ExternalOIDCWithUpstreamParity" + }, { "name": "DynamicResourceAllocation" }, diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml index 2cf2896f4677..33b9b04485c0 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml @@ -998,6 +998,19 @@ spec: claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled. maxLength: 256 type: string + expression: + description: |- + expression is an optional CEL expression used to derive + group values from JWT claims. + + CEL expressions have access to the token claims through a CEL variable, 'claims'. + + expression must be at least 1 character and must not exceed 1024 characters in length . + + When specified, claim must not be set or be explicitly set to the empty string (`""`). + maxLength: 1024 + minLength: 1 + type: string prefix: description: |- prefix is an optional field that configures the prefix that will be applied to the cluster identity attribute during the process of mapping JWT claims to cluster identity attributes. @@ -1009,8 +1022,15 @@ spec: type: string type: object x-kubernetes-validations: - - message: claim is required - rule: has(self.claim) + - message: prefix must not be set to a non-empty + value when expression is set + rule: 'has(self.expression) && size(self.expression) + > 0 ? (!has(self.prefix) || size(self.prefix) + == 0) : true' + - message: expression must not be set if claim is + specified and is not an empty string + rule: '(size(self.?claim.orValue("")) > 0) ? !has(self.expression) + : true' uid: description: |- uid is an optional field for configuring the claim mapping used to construct the uid for the cluster identity. @@ -1070,6 +1090,19 @@ spec: maxLength: 256 minLength: 1 type: string + expression: + description: |- + expression is an optional CEL expression used to derive + the username from JWT claims. + + CEL expressions have access to the token claims + through a CEL variable, 'claims'. + + expression must be at least 1 character and must not exceed 1024 characters in length. + expression must not be set when claim is set. + maxLength: 1024 + minLength: 1 + type: string prefix: description: |- prefix configures the prefix that should be prepended to the value of the JWT claim. @@ -1113,8 +1146,15 @@ spec: type: string type: object x-kubernetes-validations: - - message: claim is required - rule: has(self.claim) + - message: precisely one of claim or expression + must be set + rule: 'has(self.claim) ? !has(self.expression) + : has(self.expression)' + - message: prefixPolicy must not be set to 'Prefix' + when expression is set + rule: 'has(self.expression) && size(self.expression) + > 0 ? !has(self.prefixPolicy) || self.prefixPolicy + != ''Prefix'' : true' - message: prefix must be set if prefixPolicy is 'Prefix', but must remain unset otherwise rule: 'has(self.prefixPolicy) && self.prefixPolicy @@ -1134,6 +1174,30 @@ spec: If type is RequiredClaim, requiredClaim must be set. If Type is CEL, CEL must be set and RequiredClaim must be omitted. properties: + cel: + description: |- + cel holds the CEL expression and message for validation. + Must be set when Type is "CEL", and forbidden otherwise. + properties: + expression: + description: |- + expression is a CEL expression evaluated against token claims. + expression is required, must be at least 1 character in length and must not exceed 1024 characters. + The expression must return a boolean value where 'true' signals a valid token and 'false' an invalid one. + maxLength: 1024 + minLength: 1 + type: string + message: + description: |- + message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. + message must be at least 1 character in length and must not exceed 256 characters. + maxLength: 256 + minLength: 1 + type: string + required: + - expression + - message + type: object requiredClaim: description: |- requiredClaim allows configuring a required claim name and its expected value. @@ -1171,11 +1235,16 @@ spec: When set to 'CEL', the Kubernetes API server will be configured to validate the incoming JWT against the configured CEL expression. enum: - RequiredClaim + - CEL type: string required: - type type: object x-kubernetes-validations: + - message: cel must be set when type is 'CEL', and + forbidden otherwise + rule: 'has(self.type) && self.type == ''CEL'' ? + has(self.cel) : !has(self.cel)' - message: requiredClaim must be set when type is 'RequiredClaim', and forbidden otherwise rule: 'has(self.type) && self.type == ''RequiredClaim'' @@ -1201,6 +1270,29 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + discoveryURL: + description: |- + 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 "{issuerURL}/.well-known/openid-configuration". + + The discoveryURL must be a valid absolute HTTPS URL. + It must not contain query parameters, user information, or fragments. + Additionally, it must differ from the value of `issuerURL` (ignoring trailing slashes). + The discoveryURL value must be at least 1 character long and no longer than 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: discoveryURL must be a valid URL + rule: isURL(self) + - message: discoveryURL must be a valid https URL + rule: url(self).getScheme() == 'https' + - message: discoveryURL must not contain query parameters + rule: url(self).getQuery().size() == 0 + - message: discoveryURL must not contain fragments + rule: self.matches('^[^#]*$') + - message: discoveryURL must not contain user info + rule: '!self.matches(''^https://.+:.+@.+/.*$'')' issuerCertificateAuthority: description: |- 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. @@ -1242,6 +1334,11 @@ spec: - audiences - issuerURL type: object + x-kubernetes-validations: + - message: discoveryURL must be different from issuerURL + rule: 'self.?discoveryURL.orValue("").size() > 0 ? + (self.issuerURL.size() == 0 || self.discoveryURL.find(''^.+[^/]'') + != self.issuerURL.find(''^.+[^/]'')) : true' name: description: |- name is a required field that configures the unique human-readable identifier associated with the identity provider. @@ -1327,6 +1424,45 @@ spec: - componentNamespace - componentName x-kubernetes-list-type: map + userValidationRules: + description: |- + userValidationRules is an optional field that configures the set of rules used to validate the cluster user identity that was constructed via mapping token claims to user identity attributes. + Rules are CEL expressions that must evaluate to 'true' for authentication to succeed. + If any rule in the chain of rules evaluates to 'false', authentication will fail. + When specified, at least one rule must be specified and no more than 64 rules may be specified. + items: + description: |- + 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. + properties: + expression: + description: |- + expression is a required CEL expression that performs a validation on cluster user identity attributes like username, groups, etc. + + The expression must evaluate to a boolean value. + When the expression evaluates to 'true', the cluster user identity is considered valid. + When the expression evaluates to 'false', the cluster user identity is not considered valid. + expression must be at least 1 character in length and must not exceed 1024 characters. + maxLength: 1024 + minLength: 1 + type: string + message: + description: |- + message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. + message must be at least 1 character in length and must not exceed 256 characters. + maxLength: 256 + minLength: 1 + type: string + required: + - expression + - message + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - expression + x-kubernetes-list-type: map required: - claimMappings - issuer diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml index c167f5f107ff..779939c325ea 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml @@ -945,6 +945,19 @@ spec: claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled. maxLength: 256 type: string + expression: + description: |- + expression is an optional CEL expression used to derive + group values from JWT claims. + + CEL expressions have access to the token claims through a CEL variable, 'claims'. + + expression must be at least 1 character and must not exceed 1024 characters in length . + + When specified, claim must not be set or be explicitly set to the empty string (`""`). + maxLength: 1024 + minLength: 1 + type: string prefix: description: |- prefix is an optional field that configures the prefix that will be applied to the cluster identity attribute during the process of mapping JWT claims to cluster identity attributes. @@ -956,8 +969,15 @@ spec: type: string type: object x-kubernetes-validations: - - message: claim is required - rule: has(self.claim) + - message: prefix must not be set to a non-empty + value when expression is set + rule: 'has(self.expression) && size(self.expression) + > 0 ? (!has(self.prefix) || size(self.prefix) + == 0) : true' + - message: expression must not be set if claim is + specified and is not an empty string + rule: '(size(self.?claim.orValue("")) > 0) ? !has(self.expression) + : true' uid: description: |- uid is an optional field for configuring the claim mapping used to construct the uid for the cluster identity. @@ -1017,6 +1037,19 @@ spec: maxLength: 256 minLength: 1 type: string + expression: + description: |- + expression is an optional CEL expression used to derive + the username from JWT claims. + + CEL expressions have access to the token claims + through a CEL variable, 'claims'. + + expression must be at least 1 character and must not exceed 1024 characters in length. + expression must not be set when claim is set. + maxLength: 1024 + minLength: 1 + type: string prefix: description: |- prefix configures the prefix that should be prepended to the value of the JWT claim. @@ -1060,8 +1093,15 @@ spec: type: string type: object x-kubernetes-validations: - - message: claim is required - rule: has(self.claim) + - message: precisely one of claim or expression + must be set + rule: 'has(self.claim) ? !has(self.expression) + : has(self.expression)' + - message: prefixPolicy must not be set to 'Prefix' + when expression is set + rule: 'has(self.expression) && size(self.expression) + > 0 ? !has(self.prefixPolicy) || self.prefixPolicy + != ''Prefix'' : true' - message: prefix must be set if prefixPolicy is 'Prefix', but must remain unset otherwise rule: 'has(self.prefixPolicy) && self.prefixPolicy @@ -1081,6 +1121,30 @@ spec: If type is RequiredClaim, requiredClaim must be set. If Type is CEL, CEL must be set and RequiredClaim must be omitted. properties: + cel: + description: |- + cel holds the CEL expression and message for validation. + Must be set when Type is "CEL", and forbidden otherwise. + properties: + expression: + description: |- + expression is a CEL expression evaluated against token claims. + expression is required, must be at least 1 character in length and must not exceed 1024 characters. + The expression must return a boolean value where 'true' signals a valid token and 'false' an invalid one. + maxLength: 1024 + minLength: 1 + type: string + message: + description: |- + message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. + message must be at least 1 character in length and must not exceed 256 characters. + maxLength: 256 + minLength: 1 + type: string + required: + - expression + - message + type: object requiredClaim: description: |- requiredClaim allows configuring a required claim name and its expected value. @@ -1118,11 +1182,16 @@ spec: When set to 'CEL', the Kubernetes API server will be configured to validate the incoming JWT against the configured CEL expression. enum: - RequiredClaim + - CEL type: string required: - type type: object x-kubernetes-validations: + - message: cel must be set when type is 'CEL', and + forbidden otherwise + rule: 'has(self.type) && self.type == ''CEL'' ? + has(self.cel) : !has(self.cel)' - message: requiredClaim must be set when type is 'RequiredClaim', and forbidden otherwise rule: 'has(self.type) && self.type == ''RequiredClaim'' @@ -1148,6 +1217,29 @@ spec: minItems: 1 type: array x-kubernetes-list-type: set + discoveryURL: + description: |- + 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 "{issuerURL}/.well-known/openid-configuration". + + The discoveryURL must be a valid absolute HTTPS URL. + It must not contain query parameters, user information, or fragments. + Additionally, it must differ from the value of `issuerURL` (ignoring trailing slashes). + The discoveryURL value must be at least 1 character long and no longer than 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: discoveryURL must be a valid URL + rule: isURL(self) + - message: discoveryURL must be a valid https URL + rule: url(self).getScheme() == 'https' + - message: discoveryURL must not contain query parameters + rule: url(self).getQuery().size() == 0 + - message: discoveryURL must not contain fragments + rule: self.matches('^[^#]*$') + - message: discoveryURL must not contain user info + rule: '!self.matches(''^https://.+:.+@.+/.*$'')' issuerCertificateAuthority: description: |- 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. @@ -1189,6 +1281,11 @@ spec: - audiences - issuerURL type: object + x-kubernetes-validations: + - message: discoveryURL must be different from issuerURL + rule: 'self.?discoveryURL.orValue("").size() > 0 ? + (self.issuerURL.size() == 0 || self.discoveryURL.find(''^.+[^/]'') + != self.issuerURL.find(''^.+[^/]'')) : true' name: description: |- name is a required field that configures the unique human-readable identifier associated with the identity provider. @@ -1274,6 +1371,45 @@ spec: - componentNamespace - componentName x-kubernetes-list-type: map + userValidationRules: + description: |- + userValidationRules is an optional field that configures the set of rules used to validate the cluster user identity that was constructed via mapping token claims to user identity attributes. + Rules are CEL expressions that must evaluate to 'true' for authentication to succeed. + If any rule in the chain of rules evaluates to 'false', authentication will fail. + When specified, at least one rule must be specified and no more than 64 rules may be specified. + items: + description: |- + 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. + properties: + expression: + description: |- + expression is a required CEL expression that performs a validation on cluster user identity attributes like username, groups, etc. + + The expression must evaluate to a boolean value. + When the expression evaluates to 'true', the cluster user identity is considered valid. + When the expression evaluates to 'false', the cluster user identity is not considered valid. + expression must be at least 1 character in length and must not exceed 1024 characters. + maxLength: 1024 + minLength: 1 + type: string + message: + description: |- + message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. + message must be at least 1 character in length and must not exceed 256 characters. + maxLength: 256 + minLength: 1 + type: string + required: + - expression + - message + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - expression + x-kubernetes-list-type: map required: - claimMappings - issuer diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/auth_test.go b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/auth_test.go index ff2d54a41312..e11d8ec4f668 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/auth_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/auth_test.go @@ -137,6 +137,7 @@ func TestGenerateAuthConfig(t *testing.T) { Extra: []ExtraMapping{}, }, ClaimValidationRules: []ClaimValidationRule{}, + UserValidationRules: []UserValidationRule{}, }, }, }, diff --git a/control-plane-operator/featuregates/featuregates.go b/control-plane-operator/featuregates/featuregates.go index 7312da65682c..d62d031ca96d 100644 --- a/control-plane-operator/featuregates/featuregates.go +++ b/control-plane-operator/featuregates/featuregates.go @@ -19,7 +19,7 @@ var ( allFeatures = featuregates.NewFeatureSetAwareFeatures() externalOIDCWithUIDAndExtraClaimMappingsFeature = featuregates.NewFeature(ExternalOIDCWithUIDAndExtraClaimMappings, featuregates.WithEnableForFeatureSets(configv1.TechPreviewNoUpgrade, configv1.Default)) - externalOIDCWithUpstreamParityFeature = featuregates.NewFeature(ExternalOIDCWithUpstreamParity, featuregates.WithEnableForFeatureSets(configv1.TechPreviewNoUpgrade)) + externalOIDCWithUpstreamParityFeature = featuregates.NewFeature(ExternalOIDCWithUpstreamParity, featuregates.WithEnableForFeatureSets(configv1.TechPreviewNoUpgrade, configv1.Default)) ) func init() {