Skip to content

Commit d21e7b9

Browse files
Merge pull request #2311 from everettraven/backport-4.18/oidc-uid-extra
OCPBUGS-55792: [release-4.18] config: add uid and extra claim mappings for external OIDC configuration
2 parents 17498eb + 951d1e9 commit d21e7b9

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`.
@@ -91,6 +91,7 @@ type AuthenticationSpec struct {
9191
// +listMapKey=name
9292
// +kubebuilder:validation:MaxItems=1
9393
// +openshift:enable:FeatureGate=ExternalOIDC
94+
// +openshift:enable:FeatureGate=ExternalOIDCWithUIDAndExtraClaimMappings
9495
OIDCProviders []OIDCProvider `json:"oidcProviders,omitempty"`
9596
}
9697

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

@@ -136,7 +138,7 @@ type AuthenticationList struct {
136138
}
137139

138140
// +openshift:validation:FeatureGateAwareEnum:featureGate="",enum="";None;IntegratedOAuth
139-
// +openshift:validation:FeatureGateAwareEnum:featureGate=ExternalOIDC,enum="";None;IntegratedOAuth;OIDC
141+
// +openshift:validation:FeatureGateAwareEnum:featureGate=ExternalOIDC;ExternalOIDCWithUIDAndExtraClaimMappings,enum="";None;IntegratedOAuth;OIDC
140142
type AuthenticationType string
141143

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

273302
type TokenClaimMapping struct {
@@ -278,6 +307,110 @@ type TokenClaimMapping struct {
278307
Claim string `json:"claim"`
279308
}
280309

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

0 commit comments

Comments
 (0)