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() {