Skip to content

Commit 39c38ba

Browse files
committed
config: add uid and extra claim mappings for external OIDC configuration
Signed-off-by: Bryce Palmer <[email protected]>
1 parent 50cb52d commit 39c38ba

File tree

30 files changed

+7269
-3
lines changed

30 files changed

+7269
-3
lines changed

config/v1/tests/authentications.config.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml

Lines changed: 912 additions & 0 deletions
Large diffs are not rendered by default.

config/v1/types_authentication.go

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
55
// +genclient
66
// +genclient:nonNamespaced
77
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
8-
// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDC,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"
8+
// +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"
99

1010
// Authentication specifies cluster-wide settings for authentication (like OAuth and
1111
// webhook token authenticators). The canonical name of an instance is `cluster`.
@@ -90,6 +90,7 @@ type AuthenticationSpec struct {
9090
// +listMapKey=name
9191
// +kubebuilder:validation:MaxItems=1
9292
// +openshift:enable:FeatureGate=ExternalOIDC
93+
// +openshift:enable:FeatureGate=ExternalOIDCWithUIDAndExtraClaimMappings
9394
OIDCProviders []OIDCProvider `json:"oidcProviders,omitempty"`
9495
}
9596

@@ -117,6 +118,7 @@ type AuthenticationStatus struct {
117118
// +listMapKey=componentName
118119
// +kubebuilder:validation:MaxItems=20
119120
// +openshift:enable:FeatureGate=ExternalOIDC
121+
// +openshift:enable:FeatureGate=ExternalOIDCWithUIDAndExtraClaimMappings
120122
OIDCClients []OIDCClientStatus `json:"oidcClients"`
121123
}
122124

@@ -135,7 +137,7 @@ type AuthenticationList struct {
135137
}
136138

137139
// +openshift:validation:FeatureGateAwareEnum:featureGate="",enum="";None;IntegratedOAuth
138-
// +openshift:validation:FeatureGateAwareEnum:featureGate=ExternalOIDC,enum="";None;IntegratedOAuth;OIDC
140+
// +openshift:validation:FeatureGateAwareEnum:featureGate=ExternalOIDC;ExternalOIDCWithUIDAndExtraClaimMappings,enum="";None;IntegratedOAuth;OIDC
139141
type AuthenticationType string
140142

141143
const (
@@ -262,6 +264,33 @@ type TokenClaimMappings struct {
262264
// groups for the cluster identity.
263265
// The referenced claim must use array of strings values.
264266
Groups PrefixedClaimMapping `json:"groups,omitempty"`
267+
268+
// uid is an optional field for configuring the claim mapping
269+
// used to construct the uid for the cluster identity.
270+
//
271+
// When using uid.claim to specify the claim it must be a single string value.
272+
// When using uid.expression the expression must result in a single string value.
273+
//
274+
// When omitted, this means the user has no opinion and the platform
275+
// is left to choose a default, which is subject to change over time.
276+
// The current default is to use the 'sub' claim.
277+
//
278+
// +optional
279+
// +openshift:enable:FeatureGate=ExternalOIDCWithUIDAndExtraClaimMappings
280+
UID *TokenClaimOrExpressionMapping `json:"uid,omitempty"`
281+
282+
// extra is an optional field for configuring the mappings
283+
// used to construct the extra attribute for the cluster identity.
284+
// When omitted, no extra attributes will be present on the cluster identity.
285+
// key values for extra mappings must be unique.
286+
// A maximum of 64 extra attribute mappings may be provided.
287+
//
288+
// +optional
289+
// +kubebuilder:validation:MaxItems=64
290+
// +listType=map
291+
// +listMapKey=key
292+
// +openshift:enable:FeatureGate=ExternalOIDCWithUIDAndExtraClaimMappings
293+
Extra []ExtraMapping `json:"extra,omitempty"`
265294
}
266295

267296
type TokenClaimMapping struct {
@@ -271,6 +300,110 @@ type TokenClaimMapping struct {
271300
Claim string `json:"claim"`
272301
}
273302

303+
// TokenClaimOrExpressionMapping allows specifying either a JWT
304+
// token claim or CEL expression to be used when mapping claims
305+
// from an authentication token to cluster identities.
306+
// +kubebuilder:validation:XValidation:rule="has(self.claim) ? !has(self.expression) : has(self.expression)",message="precisely one of claim or expression must be set"
307+
type TokenClaimOrExpressionMapping struct {
308+
// claim is an optional field for specifying the
309+
// JWT token claim that is used in the mapping.
310+
// The value of this claim will be assigned to
311+
// the field in which this mapping is associated.
312+
//
313+
// Precisely one of claim or expression must be set.
314+
// claim must not be specified when expression is set.
315+
// When specified, claim must be at least 1 character in length
316+
// and must not exceed 256 characters in length.
317+
//
318+
// +optional
319+
// +kubebuilder:validation:MaxLength=256
320+
// +kubebuilder:validation:MinLength=1
321+
Claim string `json:"claim,omitempty"`
322+
323+
// expression is an optional field for specifying a
324+
// CEL expression that produces a string value from
325+
// JWT token claims.
326+
//
327+
// CEL expressions have access to the token claims
328+
// through a CEL variable, 'claims'.
329+
// 'claims' is a map of claim names to claim values.
330+
// For example, the 'sub' claim value can be accessed as 'claims.sub'.
331+
// Nested claims can be accessed using dot notation ('claims.foo.bar').
332+
//
333+
// Precisely one of claim or expression must be set.
334+
// expression must not be specified when claim is set.
335+
// When specified, expression must be at least 1 character in length
336+
// and must not exceed 4096 characters in length.
337+
//
338+
// +optional
339+
// +kubebuilder:validation:MaxLength=4096
340+
// +kubebuilder:validation:MinLength=1
341+
Expression string `json:"expression,omitempty"`
342+
}
343+
344+
// ExtraMapping allows specifying a key and CEL expression
345+
// to evaluate the keys' value. It is used to create additional
346+
// mappings and attributes added to a cluster identity from
347+
// a provided authentication token.
348+
type ExtraMapping struct {
349+
// key is a required field that specifies the string
350+
// to use as the extra attribute key.
351+
//
352+
// key must be a domain-prefix path (e.g 'example.org/foo').
353+
// key must not exceed 510 characters in length.
354+
// key must contain the '/' character, separating the domain and path characters.
355+
// key must not be empty.
356+
//
357+
// The domain portion of the key (string of characters prior to the '/') must be a valid RFC1123 subdomain.
358+
// It must not exceed 253 characters in length.
359+
// It must start and end with an alphanumeric character.
360+
// It must only contain lower case alphanumeric characters and '-' or '.'.
361+
// It must not use the reserved domains, or be subdomains of, "kubernetes.io", "k8s.io", and "openshift.io".
362+
//
363+
// The path portion of the key (string of characters after the '/') must not be empty and must consist of at least one
364+
// alphanumeric character, percent-encoded octets, '-', '.', '_', '~', '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '=', and ':'.
365+
// It must not exceed 256 characters in length.
366+
//
367+
// +required
368+
// +kubebuilder:validation:MinLength=1
369+
// +kubebuilder:validation:MaxLength=510
370+
// +kubebuilder:validation:XValidation:rule="self.contains('/')",message="key must contain the '/' character"
371+
//
372+
// +kubebuilder:validation:XValidation:rule="self.split('/', 2)[0].matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="the domain of the key must consist of only lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character"
373+
// +kubebuilder:validation:XValidation:rule="self.split('/', 2)[0].size() <= 253",message="the domain of the key must not exceed 253 characters in length"
374+
//
375+
// +kubebuilder:validation:XValidation:rule="self.split('/', 2)[0] != 'kubernetes.io'",message="the domain 'kubernetes.io' is reserved for Kubernetes use"
376+
// +kubebuilder:validation:XValidation:rule="!self.split('/', 2)[0].endsWith('.kubernetes.io')",message="the subdomains '*.kubernetes.io' are reserved for Kubernetes use"
377+
// +kubebuilder:validation:XValidation:rule="self.split('/', 2)[0] != 'k8s.io'",message="the domain 'k8s.io' is reserved for Kubernetes use"
378+
// +kubebuilder:validation:XValidation:rule="!self.split('/', 2)[0].endsWith('.k8s.io')",message="the subdomains '*.k8s.io' are reserved for Kubernetes use"
379+
// +kubebuilder:validation:XValidation:rule="self.split('/', 2)[0] != 'openshift.io'",message="the domain 'openshift.io' is reserved for OpenShift use"
380+
// +kubebuilder:validation:XValidation:rule="!self.split('/', 2)[0].endsWith('.openshift.io')",message="the subdomains '*.openshift.io' are reserved for OpenShift use"
381+
//
382+
// +kubebuilder:validation:XValidation:rule="self.split('/', 2)[1].matches('[A-Za-z0-9/\\\\-._~%!$&\\'()*+;=:]+')",message="the path of the key must not be empty and must consist of at least one alphanumeric character, percent-encoded octets, apostrophe, '-', '.', '_', '~', '!', '$', '&', '(', ')', '*', '+', ',', ';', '=', and ':'"
383+
// +kubebuilder:validation:XValidation:rule="self.split('/', 2)[1].size() <= 256",message="the path of the key must not exceed 256 characters in length"
384+
Key string `json:"key"`
385+
386+
// valueExpression is a required field to specify the CEL expression to extract
387+
// the extra attribute value from a JWT token's claims.
388+
// valueExpression must produce a string or string array value.
389+
// "", [], and null are treated as the extra mapping not being present.
390+
// Empty string values within an array are filtered out.
391+
//
392+
// CEL expressions have access to the token claims
393+
// through a CEL variable, 'claims'.
394+
// 'claims' is a map of claim names to claim values.
395+
// For example, the 'sub' claim value can be accessed as 'claims.sub'.
396+
// Nested claims can be accessed using dot notation ('claims.foo.bar').
397+
//
398+
// valueExpression must not exceed 4096 characters in length.
399+
// valueExpression must not be empty.
400+
//
401+
// +required
402+
// +kubebuilder:validation:MinLength=1
403+
// +kubebuilder:validation:MaxLength=4096
404+
ValueExpression string `json:"valueExpression"`
405+
}
406+
274407
type OIDCClientConfig struct {
275408
// componentName is the name of the component that is supposed to consume this
276409
// client configuration

0 commit comments

Comments
 (0)