Merge pull request #121078 from aramase/aramase/f/kep_3331_cel_integration
Implement CEL for StructuredAuthenticationConfig
This commit is contained in:
@@ -172,6 +172,7 @@ type JWTAuthenticator struct {
|
||||
Issuer Issuer
|
||||
ClaimValidationRules []ClaimValidationRule
|
||||
ClaimMappings ClaimMappings
|
||||
UserValidationRules []UserValidationRule
|
||||
}
|
||||
|
||||
// Issuer provides the configuration for a external provider specific settings.
|
||||
@@ -185,18 +186,43 @@ type Issuer struct {
|
||||
type ClaimValidationRule struct {
|
||||
Claim string
|
||||
RequiredValue string
|
||||
|
||||
Expression string
|
||||
Message string
|
||||
}
|
||||
|
||||
// ClaimMappings provides the configuration for claim mapping
|
||||
type ClaimMappings struct {
|
||||
Username PrefixedClaimOrExpression
|
||||
Groups PrefixedClaimOrExpression
|
||||
UID ClaimOrExpression
|
||||
Extra []ExtraMapping
|
||||
}
|
||||
|
||||
// PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression.
|
||||
type PrefixedClaimOrExpression struct {
|
||||
Claim string
|
||||
Prefix *string
|
||||
|
||||
Expression string
|
||||
}
|
||||
|
||||
// ClaimOrExpression provides the configuration for a single claim or expression.
|
||||
type ClaimOrExpression struct {
|
||||
Claim string
|
||||
Expression string
|
||||
}
|
||||
|
||||
// ExtraMapping provides the configuration for a single extra mapping.
|
||||
type ExtraMapping struct {
|
||||
Key string
|
||||
ValueExpression string
|
||||
}
|
||||
|
||||
// UserValidationRule provides the configuration for a single user validation rule.
|
||||
type UserValidationRule struct {
|
||||
Expression string
|
||||
Message string
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
@@ -192,6 +192,13 @@ type JWTAuthenticator struct {
|
||||
// claimMappings points claims of a token to be treated as user attributes.
|
||||
// +required
|
||||
ClaimMappings ClaimMappings `json:"claimMappings"`
|
||||
|
||||
// userValidationRules are rules that are applied to final user before completing authentication.
|
||||
// These allow invariants to be applied to incoming identities such as preventing the
|
||||
// use of the system: prefix that is commonly used by Kubernetes components.
|
||||
// The validation rules are logically ANDed together and must all return true for the validation to pass.
|
||||
// +optional
|
||||
UserValidationRules []UserValidationRule `json:"userValidationRules,omitempty"`
|
||||
}
|
||||
|
||||
// Issuer provides the configuration for a external provider specific settings.
|
||||
@@ -225,14 +232,36 @@ type ClaimValidationRule struct {
|
||||
// claim is the name of a required claim.
|
||||
// Same as --oidc-required-claim flag.
|
||||
// Only string claim keys are supported.
|
||||
// +required
|
||||
Claim string `json:"claim"`
|
||||
// Mutually exclusive with expression and message.
|
||||
// +optional
|
||||
Claim string `json:"claim,omitempty"`
|
||||
// requiredValue is the value of a required claim.
|
||||
// Same as --oidc-required-claim flag.
|
||||
// Only string claim values are supported.
|
||||
// If claim is set and requiredValue is not set, the claim must be present with a value set to the empty string.
|
||||
// Mutually exclusive with expression and message.
|
||||
// +optional
|
||||
RequiredValue string `json:"requiredValue"`
|
||||
RequiredValue string `json:"requiredValue,omitempty"`
|
||||
|
||||
// expression represents the expression which will be evaluated by CEL.
|
||||
// Must produce a boolean.
|
||||
//
|
||||
// CEL expressions have access to the contents of the token claims, organized into CEL variable:
|
||||
// - 'claims' is a map of claim names to claim values.
|
||||
// For example, a variable named 'sub' can be accessed as 'claims.sub'.
|
||||
// Nested claims can be accessed using dot notation, e.g. 'claims.email.verified'.
|
||||
// Must return true for the validation to pass.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// Mutually exclusive with claim and requiredValue.
|
||||
// +optional
|
||||
Expression string `json:"expression,omitempty"`
|
||||
// message customizes the returned error message when expression returns false.
|
||||
// message is a literal string.
|
||||
// Mutually exclusive with claim and requiredValue.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// ClaimMappings provides the configuration for claim mapping
|
||||
@@ -240,6 +269,7 @@ type ClaimMappings struct {
|
||||
// username represents an option for the username attribute.
|
||||
// The claim's value must be a singular string.
|
||||
// Same as the --oidc-username-claim and --oidc-username-prefix flags.
|
||||
// If username.expression is set, the expression must produce a string value.
|
||||
//
|
||||
// In the flag based approach, the --oidc-username-claim and --oidc-username-prefix are optional. If --oidc-username-claim is not set,
|
||||
// the default value is "sub". For the authentication config, there is no defaulting for claim or prefix. The claim and prefix must be set explicitly.
|
||||
@@ -254,19 +284,136 @@ type ClaimMappings struct {
|
||||
Username PrefixedClaimOrExpression `json:"username"`
|
||||
// groups represents an option for the groups attribute.
|
||||
// The claim's value must be a string or string array claim.
|
||||
// // If groups.claim is set, the prefix must be specified (and can be the empty string).
|
||||
// If groups.claim is set, the prefix must be specified (and can be the empty string).
|
||||
// If groups.expression is set, the expression must produce a string or string array value.
|
||||
// "", [], and null values are treated as the group mapping not being present.
|
||||
// +optional
|
||||
Groups PrefixedClaimOrExpression `json:"groups,omitempty"`
|
||||
|
||||
// uid represents an option for the uid attribute.
|
||||
// Claim must be a singular string claim.
|
||||
// If uid.expression is set, the expression must produce a string value.
|
||||
// +optional
|
||||
UID ClaimOrExpression `json:"uid"`
|
||||
|
||||
// extra represents an option for the extra attribute.
|
||||
// expression must produce a string or string array value.
|
||||
// If the value is empty, the extra mapping will not be present.
|
||||
//
|
||||
// hard-coded extra key/value
|
||||
// - key: "foo"
|
||||
// valueExpression: "'bar'"
|
||||
// This will result in an extra attribute - foo: ["bar"]
|
||||
//
|
||||
// hard-coded key, value copying claim value
|
||||
// - key: "foo"
|
||||
// valueExpression: "claims.some_claim"
|
||||
// This will result in an extra attribute - foo: [value of some_claim]
|
||||
//
|
||||
// hard-coded key, value derived from claim value
|
||||
// - key: "admin"
|
||||
// valueExpression: '(has(claims.is_admin) && claims.is_admin) ? "true":""'
|
||||
// This will result in:
|
||||
// - if is_admin claim is present and true, extra attribute - admin: ["true"]
|
||||
// - if is_admin claim is present and false or is_admin claim is not present, no extra attribute will be added
|
||||
//
|
||||
// +optional
|
||||
Extra []ExtraMapping `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression.
|
||||
type PrefixedClaimOrExpression struct {
|
||||
// claim is the JWT claim to use.
|
||||
// Mutually exclusive with expression.
|
||||
// +optional
|
||||
Claim string `json:"claim"`
|
||||
Claim string `json:"claim,omitempty"`
|
||||
// prefix is prepended to claim's value to prevent clashes with existing names.
|
||||
// prefix needs to be set if claim is set and can be the empty string.
|
||||
// Mutually exclusive with expression.
|
||||
// +optional
|
||||
Prefix *string `json:"prefix,omitempty"`
|
||||
|
||||
// expression represents the expression which will be evaluated by CEL.
|
||||
//
|
||||
// CEL expressions have access to the contents of the token claims, organized into CEL variable:
|
||||
// - 'claims' is a map of claim names to claim values.
|
||||
// For example, a variable named 'sub' can be accessed as 'claims.sub'.
|
||||
// Nested claims can be accessed using dot notation, e.g. 'claims.email.verified'.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// Mutually exclusive with claim and prefix.
|
||||
// +optional
|
||||
Expression string `json:"expression,omitempty"`
|
||||
}
|
||||
|
||||
// ClaimOrExpression provides the configuration for a single claim or expression.
|
||||
type ClaimOrExpression struct {
|
||||
// claim is the JWT claim to use.
|
||||
// Either claim or expression must be set.
|
||||
// Mutually exclusive with expression.
|
||||
// +optional
|
||||
Claim string `json:"claim,omitempty"`
|
||||
|
||||
// expression represents the expression which will be evaluated by CEL.
|
||||
//
|
||||
// CEL expressions have access to the contents of the token claims, organized into CEL variable:
|
||||
// - 'claims' is a map of claim names to claim values.
|
||||
// For example, a variable named 'sub' can be accessed as 'claims.sub'.
|
||||
// Nested claims can be accessed using dot notation, e.g. 'claims.email.verified'.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// Mutually exclusive with claim.
|
||||
// +optional
|
||||
Expression string `json:"expression,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraMapping provides the configuration for a single extra mapping.
|
||||
type ExtraMapping struct {
|
||||
// key is a string to use as the extra attribute key.
|
||||
// key must be a domain-prefix path (e.g. example.org/foo). All characters before the first "/" must be a valid
|
||||
// subdomain as defined by RFC 1123. All characters trailing the first "/" must
|
||||
// be valid HTTP Path characters as defined by RFC 3986.
|
||||
// key must be lowercase.
|
||||
// +required
|
||||
Prefix *string `json:"prefix"`
|
||||
Key string `json:"key"`
|
||||
|
||||
// valueExpression is a CEL expression to extract extra attribute value.
|
||||
// valueExpression must produce a string or string array value.
|
||||
// "", [], and null values are treated as the extra mapping not being present.
|
||||
// Empty string values contained within a string array are filtered out.
|
||||
//
|
||||
// CEL expressions have access to the contents of the token claims, organized into CEL variable:
|
||||
// - 'claims' is a map of claim names to claim values.
|
||||
// For example, a variable named 'sub' can be accessed as 'claims.sub'.
|
||||
// Nested claims can be accessed using dot notation, e.g. 'claims.email.verified'.
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// +required
|
||||
ValueExpression string `json:"valueExpression"`
|
||||
}
|
||||
|
||||
// UserValidationRule provides the configuration for a single user info validation rule.
|
||||
type UserValidationRule struct {
|
||||
// expression represents the expression which will be evaluated by CEL.
|
||||
// Must return true for the validation to pass.
|
||||
//
|
||||
// CEL expressions have access to the contents of UserInfo, organized into CEL variable:
|
||||
// - 'user' - authentication.k8s.io/v1, Kind=UserInfo object
|
||||
// Refer to https://github.com/kubernetes/api/blob/release-1.28/authentication/v1/types.go#L105-L122 for the definition.
|
||||
// API documentation: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#userinfo-v1-authentication-k8s-io
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// +required
|
||||
Expression string `json:"expression"`
|
||||
|
||||
// message customizes the returned error message when rule returns false.
|
||||
// message is a literal string.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
@@ -96,6 +96,16 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*ClaimOrExpression)(nil), (*apiserver.ClaimOrExpression)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_ClaimOrExpression_To_apiserver_ClaimOrExpression(a.(*ClaimOrExpression), b.(*apiserver.ClaimOrExpression), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*apiserver.ClaimOrExpression)(nil), (*ClaimOrExpression)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_apiserver_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(a.(*apiserver.ClaimOrExpression), b.(*ClaimOrExpression), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*ClaimValidationRule)(nil), (*apiserver.ClaimValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_ClaimValidationRule_To_apiserver_ClaimValidationRule(a.(*ClaimValidationRule), b.(*apiserver.ClaimValidationRule), scope)
|
||||
}); err != nil {
|
||||
@@ -131,6 +141,16 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*ExtraMapping)(nil), (*apiserver.ExtraMapping)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_ExtraMapping_To_apiserver_ExtraMapping(a.(*ExtraMapping), b.(*apiserver.ExtraMapping), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*apiserver.ExtraMapping)(nil), (*ExtraMapping)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_apiserver_ExtraMapping_To_v1alpha1_ExtraMapping(a.(*apiserver.ExtraMapping), b.(*ExtraMapping), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*Issuer)(nil), (*apiserver.Issuer)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_Issuer_To_apiserver_Issuer(a.(*Issuer), b.(*apiserver.Issuer), scope)
|
||||
}); err != nil {
|
||||
@@ -211,6 +231,16 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*UserValidationRule)(nil), (*apiserver.UserValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_UserValidationRule_To_apiserver_UserValidationRule(a.(*UserValidationRule), b.(*apiserver.UserValidationRule), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*apiserver.UserValidationRule)(nil), (*UserValidationRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_apiserver_UserValidationRule_To_v1alpha1_UserValidationRule(a.(*apiserver.UserValidationRule), b.(*UserValidationRule), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*WebhookConfiguration)(nil), (*apiserver.WebhookConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_WebhookConfiguration_To_apiserver_WebhookConfiguration(a.(*WebhookConfiguration), b.(*apiserver.WebhookConfiguration), scope)
|
||||
}); err != nil {
|
||||
@@ -364,6 +394,10 @@ func autoConvert_v1alpha1_ClaimMappings_To_apiserver_ClaimMappings(in *ClaimMapp
|
||||
if err := Convert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression(&in.Groups, &out.Groups, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := Convert_v1alpha1_ClaimOrExpression_To_apiserver_ClaimOrExpression(&in.UID, &out.UID, s); err != nil {
|
||||
return err
|
||||
}
|
||||
out.Extra = *(*[]apiserver.ExtraMapping)(unsafe.Pointer(&in.Extra))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -379,6 +413,10 @@ func autoConvert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(in *apiserver
|
||||
if err := Convert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(&in.Groups, &out.Groups, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := Convert_apiserver_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(&in.UID, &out.UID, s); err != nil {
|
||||
return err
|
||||
}
|
||||
out.Extra = *(*[]ExtraMapping)(unsafe.Pointer(&in.Extra))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -387,9 +425,33 @@ func Convert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(in *apiserver.Cla
|
||||
return autoConvert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_ClaimOrExpression_To_apiserver_ClaimOrExpression(in *ClaimOrExpression, out *apiserver.ClaimOrExpression, s conversion.Scope) error {
|
||||
out.Claim = in.Claim
|
||||
out.Expression = in.Expression
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_ClaimOrExpression_To_apiserver_ClaimOrExpression is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_ClaimOrExpression_To_apiserver_ClaimOrExpression(in *ClaimOrExpression, out *apiserver.ClaimOrExpression, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_ClaimOrExpression_To_apiserver_ClaimOrExpression(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_apiserver_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(in *apiserver.ClaimOrExpression, out *ClaimOrExpression, s conversion.Scope) error {
|
||||
out.Claim = in.Claim
|
||||
out.Expression = in.Expression
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_apiserver_ClaimOrExpression_To_v1alpha1_ClaimOrExpression is an autogenerated conversion function.
|
||||
func Convert_apiserver_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(in *apiserver.ClaimOrExpression, out *ClaimOrExpression, s conversion.Scope) error {
|
||||
return autoConvert_apiserver_ClaimOrExpression_To_v1alpha1_ClaimOrExpression(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_ClaimValidationRule_To_apiserver_ClaimValidationRule(in *ClaimValidationRule, out *apiserver.ClaimValidationRule, s conversion.Scope) error {
|
||||
out.Claim = in.Claim
|
||||
out.RequiredValue = in.RequiredValue
|
||||
out.Expression = in.Expression
|
||||
out.Message = in.Message
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -401,6 +463,8 @@ func Convert_v1alpha1_ClaimValidationRule_To_apiserver_ClaimValidationRule(in *C
|
||||
func autoConvert_apiserver_ClaimValidationRule_To_v1alpha1_ClaimValidationRule(in *apiserver.ClaimValidationRule, out *ClaimValidationRule, s conversion.Scope) error {
|
||||
out.Claim = in.Claim
|
||||
out.RequiredValue = in.RequiredValue
|
||||
out.Expression = in.Expression
|
||||
out.Message = in.Message
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -492,6 +556,28 @@ func Convert_apiserver_EgressSelectorConfiguration_To_v1alpha1_EgressSelectorCon
|
||||
return autoConvert_apiserver_EgressSelectorConfiguration_To_v1alpha1_EgressSelectorConfiguration(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_ExtraMapping_To_apiserver_ExtraMapping(in *ExtraMapping, out *apiserver.ExtraMapping, s conversion.Scope) error {
|
||||
out.Key = in.Key
|
||||
out.ValueExpression = in.ValueExpression
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_ExtraMapping_To_apiserver_ExtraMapping is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_ExtraMapping_To_apiserver_ExtraMapping(in *ExtraMapping, out *apiserver.ExtraMapping, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_ExtraMapping_To_apiserver_ExtraMapping(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_apiserver_ExtraMapping_To_v1alpha1_ExtraMapping(in *apiserver.ExtraMapping, out *ExtraMapping, s conversion.Scope) error {
|
||||
out.Key = in.Key
|
||||
out.ValueExpression = in.ValueExpression
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_apiserver_ExtraMapping_To_v1alpha1_ExtraMapping is an autogenerated conversion function.
|
||||
func Convert_apiserver_ExtraMapping_To_v1alpha1_ExtraMapping(in *apiserver.ExtraMapping, out *ExtraMapping, s conversion.Scope) error {
|
||||
return autoConvert_apiserver_ExtraMapping_To_v1alpha1_ExtraMapping(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_Issuer_To_apiserver_Issuer(in *Issuer, out *apiserver.Issuer, s conversion.Scope) error {
|
||||
out.URL = in.URL
|
||||
out.CertificateAuthority = in.CertificateAuthority
|
||||
@@ -524,6 +610,7 @@ func autoConvert_v1alpha1_JWTAuthenticator_To_apiserver_JWTAuthenticator(in *JWT
|
||||
if err := Convert_v1alpha1_ClaimMappings_To_apiserver_ClaimMappings(&in.ClaimMappings, &out.ClaimMappings, s); err != nil {
|
||||
return err
|
||||
}
|
||||
out.UserValidationRules = *(*[]apiserver.UserValidationRule)(unsafe.Pointer(&in.UserValidationRules))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -540,6 +627,7 @@ func autoConvert_apiserver_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(in *api
|
||||
if err := Convert_apiserver_ClaimMappings_To_v1alpha1_ClaimMappings(&in.ClaimMappings, &out.ClaimMappings, s); err != nil {
|
||||
return err
|
||||
}
|
||||
out.UserValidationRules = *(*[]UserValidationRule)(unsafe.Pointer(&in.UserValidationRules))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -551,6 +639,7 @@ func Convert_apiserver_JWTAuthenticator_To_v1alpha1_JWTAuthenticator(in *apiserv
|
||||
func autoConvert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpression(in *PrefixedClaimOrExpression, out *apiserver.PrefixedClaimOrExpression, s conversion.Scope) error {
|
||||
out.Claim = in.Claim
|
||||
out.Prefix = (*string)(unsafe.Pointer(in.Prefix))
|
||||
out.Expression = in.Expression
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -562,6 +651,7 @@ func Convert_v1alpha1_PrefixedClaimOrExpression_To_apiserver_PrefixedClaimOrExpr
|
||||
func autoConvert_apiserver_PrefixedClaimOrExpression_To_v1alpha1_PrefixedClaimOrExpression(in *apiserver.PrefixedClaimOrExpression, out *PrefixedClaimOrExpression, s conversion.Scope) error {
|
||||
out.Claim = in.Claim
|
||||
out.Prefix = (*string)(unsafe.Pointer(in.Prefix))
|
||||
out.Expression = in.Expression
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -678,6 +768,28 @@ func Convert_apiserver_UDSTransport_To_v1alpha1_UDSTransport(in *apiserver.UDSTr
|
||||
return autoConvert_apiserver_UDSTransport_To_v1alpha1_UDSTransport(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_UserValidationRule_To_apiserver_UserValidationRule(in *UserValidationRule, out *apiserver.UserValidationRule, s conversion.Scope) error {
|
||||
out.Expression = in.Expression
|
||||
out.Message = in.Message
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_UserValidationRule_To_apiserver_UserValidationRule is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_UserValidationRule_To_apiserver_UserValidationRule(in *UserValidationRule, out *apiserver.UserValidationRule, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_UserValidationRule_To_apiserver_UserValidationRule(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_apiserver_UserValidationRule_To_v1alpha1_UserValidationRule(in *apiserver.UserValidationRule, out *UserValidationRule, s conversion.Scope) error {
|
||||
out.Expression = in.Expression
|
||||
out.Message = in.Message
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_apiserver_UserValidationRule_To_v1alpha1_UserValidationRule is an autogenerated conversion function.
|
||||
func Convert_apiserver_UserValidationRule_To_v1alpha1_UserValidationRule(in *apiserver.UserValidationRule, out *UserValidationRule, s conversion.Scope) error {
|
||||
return autoConvert_apiserver_UserValidationRule_To_v1alpha1_UserValidationRule(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_WebhookConfiguration_To_apiserver_WebhookConfiguration(in *WebhookConfiguration, out *apiserver.WebhookConfiguration, s conversion.Scope) error {
|
||||
out.AuthorizedTTL = in.AuthorizedTTL
|
||||
out.UnauthorizedTTL = in.UnauthorizedTTL
|
||||
|
||||
@@ -168,6 +168,12 @@ func (in *ClaimMappings) DeepCopyInto(out *ClaimMappings) {
|
||||
*out = *in
|
||||
in.Username.DeepCopyInto(&out.Username)
|
||||
in.Groups.DeepCopyInto(&out.Groups)
|
||||
out.UID = in.UID
|
||||
if in.Extra != nil {
|
||||
in, out := &in.Extra, &out.Extra
|
||||
*out = make([]ExtraMapping, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,6 +187,22 @@ func (in *ClaimMappings) DeepCopy() *ClaimMappings {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClaimOrExpression) DeepCopyInto(out *ClaimOrExpression) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimOrExpression.
|
||||
func (in *ClaimOrExpression) DeepCopy() *ClaimOrExpression {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ClaimOrExpression)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClaimValidationRule) DeepCopyInto(out *ClaimValidationRule) {
|
||||
*out = *in
|
||||
@@ -267,6 +289,22 @@ func (in *EgressSelectorConfiguration) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ExtraMapping) DeepCopyInto(out *ExtraMapping) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraMapping.
|
||||
func (in *ExtraMapping) DeepCopy() *ExtraMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ExtraMapping)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Issuer) DeepCopyInto(out *Issuer) {
|
||||
*out = *in
|
||||
@@ -298,6 +336,11 @@ func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) {
|
||||
copy(*out, *in)
|
||||
}
|
||||
in.ClaimMappings.DeepCopyInto(&out.ClaimMappings)
|
||||
if in.UserValidationRules != nil {
|
||||
in, out := &in.UserValidationRules, &out.UserValidationRules
|
||||
*out = make([]UserValidationRule, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -437,6 +480,22 @@ func (in *UDSTransport) DeepCopy() *UDSTransport {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UserValidationRule) DeepCopyInto(out *UserValidationRule) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserValidationRule.
|
||||
func (in *UserValidationRule) DeepCopy() *UserValidationRule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UserValidationRule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *WebhookConfiguration) DeepCopyInto(out *WebhookConfiguration) {
|
||||
*out = *in
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/apiserver/pkg/apis/apiserver"
|
||||
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
|
||||
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
@@ -75,26 +76,32 @@ func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration) fie
|
||||
// check and add validation for duplicate issuers.
|
||||
for i, a := range c.JWT {
|
||||
fldPath := root.Index(i)
|
||||
allErrs = append(allErrs, validateJWTAuthenticator(a, fldPath)...)
|
||||
_, errs := validateJWTAuthenticator(a, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
|
||||
allErrs = append(allErrs, errs...)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateJWTAuthenticator validates a given JWTAuthenticator.
|
||||
// CompileAndValidateJWTAuthenticator validates a given JWTAuthenticator and returns a CELMapper with the compiled
|
||||
// CEL expressions for claim mappings and validation rules.
|
||||
// This is exported for use in oidc package.
|
||||
func ValidateJWTAuthenticator(authenticator api.JWTAuthenticator) field.ErrorList {
|
||||
return validateJWTAuthenticator(authenticator, nil)
|
||||
func CompileAndValidateJWTAuthenticator(authenticator api.JWTAuthenticator) (authenticationcel.CELMapper, field.ErrorList) {
|
||||
return validateJWTAuthenticator(authenticator, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
|
||||
}
|
||||
|
||||
func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path) field.ErrorList {
|
||||
func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path, structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) {
|
||||
var allErrs field.ErrorList
|
||||
|
||||
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, fldPath.Child("issuer"))...)
|
||||
allErrs = append(allErrs, validateClaimValidationRules(authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"))...)
|
||||
allErrs = append(allErrs, validateClaimMappings(authenticator.ClaimMappings, fldPath.Child("claimMappings"))...)
|
||||
compiler := authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
mapper := &authenticationcel.CELMapper{}
|
||||
|
||||
return allErrs
|
||||
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, fldPath.Child("issuer"))...)
|
||||
allErrs = append(allErrs, validateClaimValidationRules(compiler, mapper, authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"), structuredAuthnFeatureEnabled)...)
|
||||
allErrs = append(allErrs, validateClaimMappings(compiler, mapper, authenticator.ClaimMappings, fldPath.Child("claimMappings"), structuredAuthnFeatureEnabled)...)
|
||||
allErrs = append(allErrs, validateUserValidationRules(compiler, mapper, authenticator.UserValidationRules, fldPath.Child("userValidationRules"), structuredAuthnFeatureEnabled)...)
|
||||
|
||||
return *mapper, allErrs
|
||||
}
|
||||
|
||||
func validateIssuer(issuer api.Issuer, fldPath *field.Path) field.ErrorList {
|
||||
@@ -174,48 +181,250 @@ func validateCertificateAuthority(certificateAuthority string, fldPath *field.Pa
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateClaimValidationRules(rules []api.ClaimValidationRule, fldPath *field.Path) field.ErrorList {
|
||||
func validateClaimValidationRules(compiler authenticationcel.Compiler, celMapper *authenticationcel.CELMapper, rules []api.ClaimValidationRule, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
|
||||
seenClaims := sets.NewString()
|
||||
seenExpressions := sets.NewString()
|
||||
var compilationResults []authenticationcel.CompilationResult
|
||||
|
||||
for i, rule := range rules {
|
||||
fldPath := fldPath.Index(i)
|
||||
|
||||
if len(rule.Claim) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("claim"), "claim name is required"))
|
||||
continue
|
||||
if len(rule.Expression) > 0 && !structuredAuthnFeatureEnabled {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("expression"), rule.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(rule.Claim) > 0 && len(rule.Expression) > 0:
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, rule.Claim, "claim and expression can't both be set"))
|
||||
case len(rule.Claim) == 0 && len(rule.Expression) == 0:
|
||||
allErrs = append(allErrs, field.Required(fldPath, "claim or expression is required"))
|
||||
case len(rule.Claim) > 0:
|
||||
if len(rule.Message) > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("message"), rule.Message, "message can't be set when claim is set"))
|
||||
}
|
||||
if seenClaims.Has(rule.Claim) {
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath.Child("claim"), rule.Claim))
|
||||
continue
|
||||
}
|
||||
seenClaims.Insert(rule.Claim)
|
||||
case len(rule.Expression) > 0:
|
||||
if len(rule.RequiredValue) > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("requiredValue"), rule.RequiredValue, "requiredValue can't be set when expression is set"))
|
||||
}
|
||||
if seenExpressions.Has(rule.Expression) {
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath.Child("expression"), rule.Expression))
|
||||
continue
|
||||
}
|
||||
seenExpressions.Insert(rule.Expression)
|
||||
|
||||
compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ClaimValidationCondition{
|
||||
Expression: rule.Expression,
|
||||
}, fldPath.Child("expression"))
|
||||
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
continue
|
||||
}
|
||||
if compilationResult != nil {
|
||||
compilationResults = append(compilationResults, *compilationResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if structuredAuthnFeatureEnabled && len(compilationResults) > 0 {
|
||||
celMapper.ClaimValidationRules = authenticationcel.NewClaimsMapper(compilationResults)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateClaimMappings(m api.ClaimMappings, fldPath *field.Path) field.ErrorList {
|
||||
func validateClaimMappings(compiler authenticationcel.Compiler, celMapper *authenticationcel.CELMapper, m api.ClaimMappings, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
|
||||
if len(m.Username.Claim) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("username", "claim"), "claim name is required"))
|
||||
if !structuredAuthnFeatureEnabled {
|
||||
if len(m.Username.Expression) > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("username").Child("expression"), m.Username.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
|
||||
}
|
||||
// TODO(aramase): when Expression is added to PrefixedClaimOrExpression, check prefix and expression are not both set.
|
||||
if m.Username.Prefix == nil {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("username", "prefix"), "prefix is required"))
|
||||
if len(m.Groups.Expression) > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("groups").Child("expression"), m.Groups.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
|
||||
}
|
||||
if len(m.Groups.Claim) > 0 && m.Groups.Prefix == nil {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("groups", "prefix"), "prefix is required when claim is set"))
|
||||
if len(m.UID.Claim) > 0 || len(m.UID.Expression) > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), "", "uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
|
||||
}
|
||||
if m.Groups.Prefix != nil && len(m.Groups.Claim) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("groups", "claim"), "non-empty claim name is required when prefix is set"))
|
||||
if len(m.Extra) > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("extra"), "", "extra claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
|
||||
}
|
||||
}
|
||||
|
||||
compilationResult, err := validatePrefixClaimOrExpression(compiler, m.Username, fldPath.Child("username"), true, structuredAuthnFeatureEnabled)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err...)
|
||||
} else if compilationResult != nil && structuredAuthnFeatureEnabled {
|
||||
celMapper.Username = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult})
|
||||
}
|
||||
|
||||
compilationResult, err = validatePrefixClaimOrExpression(compiler, m.Groups, fldPath.Child("groups"), false, structuredAuthnFeatureEnabled)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err...)
|
||||
} else if compilationResult != nil && structuredAuthnFeatureEnabled {
|
||||
celMapper.Groups = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult})
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(m.UID.Claim) > 0 && len(m.UID.Expression) > 0:
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), "", "claim and expression can't both be set"))
|
||||
case len(m.UID.Expression) > 0:
|
||||
compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ClaimMappingExpression{
|
||||
Expression: m.UID.Expression,
|
||||
}, fldPath.Child("uid").Child("expression"))
|
||||
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
} else if structuredAuthnFeatureEnabled && compilationResult != nil {
|
||||
celMapper.UID = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult})
|
||||
}
|
||||
}
|
||||
|
||||
var extraCompilationResults []authenticationcel.CompilationResult
|
||||
seenExtraKeys := sets.NewString()
|
||||
|
||||
for i, mapping := range m.Extra {
|
||||
fldPath := fldPath.Child("extra").Index(i)
|
||||
// Key should be namespaced to the authenticator or authenticator/authorizer pair making use of them.
|
||||
// For instance: "example.org/foo" instead of "foo".
|
||||
// xref: https://github.com/kubernetes/kubernetes/blob/3825e206cb162a7ad7431a5bdf6a065ae8422cf7/staging/src/k8s.io/apiserver/pkg/authentication/user/user.go#L31-L41
|
||||
// IsDomainPrefixedPath checks for non-empty key and that the key is prefixed with a domain name.
|
||||
allErrs = append(allErrs, utilvalidation.IsDomainPrefixedPath(fldPath.Child("key"), mapping.Key)...)
|
||||
if mapping.Key != strings.ToLower(mapping.Key) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), mapping.Key, "key must be lowercase"))
|
||||
}
|
||||
if seenExtraKeys.Has(mapping.Key) {
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath.Child("key"), mapping.Key))
|
||||
continue
|
||||
}
|
||||
seenExtraKeys.Insert(mapping.Key)
|
||||
|
||||
if len(mapping.ValueExpression) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("valueExpression"), "valueExpression is required"))
|
||||
continue
|
||||
}
|
||||
|
||||
compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ExtraMappingExpression{
|
||||
Key: mapping.Key,
|
||||
Expression: mapping.ValueExpression,
|
||||
}, fldPath.Child("valueExpression"))
|
||||
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if compilationResult != nil {
|
||||
extraCompilationResults = append(extraCompilationResults, *compilationResult)
|
||||
}
|
||||
}
|
||||
|
||||
if structuredAuthnFeatureEnabled && len(extraCompilationResults) > 0 {
|
||||
celMapper.Extra = authenticationcel.NewClaimsMapper(extraCompilationResults)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validatePrefixClaimOrExpression(compiler authenticationcel.Compiler, mapping api.PrefixedClaimOrExpression, fldPath *field.Path, claimOrExpressionRequired, structuredAuthnFeatureEnabled bool) (*authenticationcel.CompilationResult, field.ErrorList) {
|
||||
var allErrs field.ErrorList
|
||||
|
||||
var compilationResult *authenticationcel.CompilationResult
|
||||
switch {
|
||||
case len(mapping.Expression) > 0 && len(mapping.Claim) > 0:
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, "", "claim and expression can't both be set"))
|
||||
case len(mapping.Expression) == 0 && len(mapping.Claim) == 0 && claimOrExpressionRequired:
|
||||
allErrs = append(allErrs, field.Required(fldPath, "claim or expression is required"))
|
||||
case len(mapping.Expression) > 0:
|
||||
var err *field.Error
|
||||
|
||||
if mapping.Prefix != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("prefix"), *mapping.Prefix, "prefix can't be set when expression is set"))
|
||||
}
|
||||
compilationResult, err = compileClaimsCELExpression(compiler, &authenticationcel.ClaimMappingExpression{
|
||||
Expression: mapping.Expression,
|
||||
}, fldPath.Child("expression"))
|
||||
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
|
||||
case len(mapping.Claim) > 0:
|
||||
if mapping.Prefix == nil {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("prefix"), "prefix is required when claim is set. It can be set to an empty string to disable prefixing"))
|
||||
}
|
||||
}
|
||||
|
||||
return compilationResult, allErrs
|
||||
}
|
||||
|
||||
func validateUserValidationRules(compiler authenticationcel.Compiler, celMapper *authenticationcel.CELMapper, rules []api.UserValidationRule, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
var compilationResults []authenticationcel.CompilationResult
|
||||
|
||||
if len(rules) > 0 && !structuredAuthnFeatureEnabled {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, "", "user validation rules are not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
|
||||
}
|
||||
|
||||
seenExpressions := sets.NewString()
|
||||
for i, rule := range rules {
|
||||
fldPath := fldPath.Index(i)
|
||||
|
||||
if len(rule.Expression) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("expression"), "expression is required"))
|
||||
continue
|
||||
}
|
||||
|
||||
if seenExpressions.Has(rule.Expression) {
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath.Child("expression"), rule.Expression))
|
||||
continue
|
||||
}
|
||||
seenExpressions.Insert(rule.Expression)
|
||||
|
||||
compilationResult, err := compileUserCELExpression(compiler, &authenticationcel.UserValidationCondition{
|
||||
Expression: rule.Expression,
|
||||
Message: rule.Message,
|
||||
}, fldPath.Child("expression"))
|
||||
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if compilationResult != nil {
|
||||
compilationResults = append(compilationResults, *compilationResult)
|
||||
}
|
||||
}
|
||||
|
||||
if structuredAuthnFeatureEnabled && len(compilationResults) > 0 {
|
||||
celMapper.UserValidationRules = authenticationcel.NewUserMapper(compilationResults)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func compileClaimsCELExpression(compiler authenticationcel.Compiler, expression authenticationcel.ExpressionAccessor, fldPath *field.Path) (*authenticationcel.CompilationResult, *field.Error) {
|
||||
compilationResult, err := compiler.CompileClaimsExpression(expression)
|
||||
if err != nil {
|
||||
return nil, convertCELErrorToValidationError(fldPath, expression, err)
|
||||
}
|
||||
return &compilationResult, nil
|
||||
}
|
||||
|
||||
func compileUserCELExpression(compiler authenticationcel.Compiler, expression authenticationcel.ExpressionAccessor, fldPath *field.Path) (*authenticationcel.CompilationResult, *field.Error) {
|
||||
compilationResult, err := compiler.CompileUserExpression(expression)
|
||||
if err != nil {
|
||||
return nil, convertCELErrorToValidationError(fldPath, expression, err)
|
||||
}
|
||||
return &compilationResult, nil
|
||||
}
|
||||
|
||||
// ValidateAuthorizationConfiguration validates a given AuthorizationConfiguration.
|
||||
func ValidateAuthorizationConfiguration(fldPath *field.Path, c *api.AuthorizationConfiguration, knownTypes sets.String, repeatableTypes sets.String) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -32,6 +33,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/apiserver/pkg/apis/apiserver"
|
||||
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
@@ -39,7 +42,13 @@ import (
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var (
|
||||
compiler = authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
)
|
||||
|
||||
func TestValidateAuthenticationConfiguration(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
in *api.AuthenticationConfiguration
|
||||
@@ -133,7 +142,37 @@ func TestValidateAuthenticationConfiguration(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "jwt[0].claimMappings.username.claim: Required value: claim name is required",
|
||||
want: "jwt[0].claimMappings.username: Required value: claim or expression is required",
|
||||
},
|
||||
{
|
||||
name: "failed userValidationRule validation",
|
||||
in: &api.AuthenticationConfiguration{
|
||||
JWT: []api.JWTAuthenticator{
|
||||
{
|
||||
Issuer: api.Issuer{
|
||||
URL: "https://issuer-url",
|
||||
Audiences: []string{"audience"},
|
||||
},
|
||||
ClaimValidationRules: []api.ClaimValidationRule{
|
||||
{
|
||||
Claim: "foo",
|
||||
RequiredValue: "bar",
|
||||
},
|
||||
},
|
||||
ClaimMappings: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "sub",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
},
|
||||
UserValidationRules: []api.UserValidationRule{
|
||||
{Expression: "user.username == 'foo'"},
|
||||
{Expression: "user.username == 'foo'"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `jwt[0].userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`,
|
||||
},
|
||||
{
|
||||
name: "valid authentication configuration",
|
||||
@@ -313,31 +352,98 @@ func TestValidateCertificateAuthority(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimValidationRules(t *testing.T) {
|
||||
func TestValidateClaimValidationRules(t *testing.T) {
|
||||
fldPath := field.NewPath("issuer", "claimValidationRules")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
in []api.ClaimValidationRule
|
||||
structuredAuthnFeatureEnabled bool
|
||||
want string
|
||||
wantCELMapper bool
|
||||
}{
|
||||
{
|
||||
name: "claim validation rule claim is empty",
|
||||
in: []api.ClaimValidationRule{{Claim: ""}},
|
||||
want: "issuer.claimValidationRules[0].claim: Required value: claim name is required",
|
||||
name: "claim and expression are empty, structured authn feature enabled",
|
||||
in: []api.ClaimValidationRule{{}},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: "issuer.claimValidationRules[0]: Required value: claim or expression is required",
|
||||
},
|
||||
{
|
||||
name: "claim and expression are set",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Claim: "claim", Expression: "expression"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimValidationRules[0]: Invalid value: "claim": claim and expression can't both be set`,
|
||||
},
|
||||
{
|
||||
name: "message set when claim is set",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Claim: "claim", Message: "message"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimValidationRules[0].message: Invalid value: "message": message can't be set when claim is set`,
|
||||
},
|
||||
{
|
||||
name: "requiredValue set when expression is set",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Expression: "claims.foo == 'bar'", RequiredValue: "value"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimValidationRules[0].requiredValue: Invalid value: "value": requiredValue can't be set when expression is set`,
|
||||
},
|
||||
{
|
||||
name: "duplicate claim",
|
||||
in: []api.ClaimValidationRule{{
|
||||
Claim: "claim", RequiredValue: "value1"},
|
||||
{Claim: "claim", RequiredValue: "value2"},
|
||||
in: []api.ClaimValidationRule{
|
||||
{Claim: "claim"},
|
||||
{Claim: "claim"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`,
|
||||
},
|
||||
{
|
||||
name: "valid claim validation rule",
|
||||
in: []api.ClaimValidationRule{{Claim: "claim", RequiredValue: "value"}},
|
||||
name: "duplicate expression",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Expression: "claims.foo == 'bar'"},
|
||||
{Expression: "claims.foo == 'bar'"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimValidationRules[1].expression: Duplicate value: "claims.foo == 'bar'"`,
|
||||
},
|
||||
{
|
||||
name: "expression set when structured authn feature is disabled",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Expression: "claims.foo == 'bar'"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo == 'bar'": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
||||
},
|
||||
{
|
||||
name: "CEL expression compilation error",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Expression: "foo.bar"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimValidationRules[0].expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "expression does not evaluate to bool",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Expression: "claims.foo"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo": must evaluate to bool`,
|
||||
},
|
||||
{
|
||||
name: "valid claim validation rule with expression",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Expression: "claims.foo == 'bar'"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: "",
|
||||
wantCELMapper: true,
|
||||
},
|
||||
{
|
||||
name: "valid claim validation rule with multiple rules",
|
||||
@@ -345,16 +451,21 @@ func TestClaimValidationRules(t *testing.T) {
|
||||
{Claim: "claim1", RequiredValue: "value1"},
|
||||
{Claim: "claim2", RequiredValue: "value2"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := validateClaimValidationRules(tt.in, fldPath).ToAggregate()
|
||||
celMapper := &authenticationcel.CELMapper{}
|
||||
got := validateClaimValidationRules(compiler, celMapper, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
||||
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
||||
t.Fatalf("ClaimValidationRules validation mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
if tt.wantCELMapper && celMapper.ClaimValidationRules == nil {
|
||||
t.Fatalf("ClaimValidationRules validation mismatch: CELMapper.ClaimValidationRules is nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -365,50 +476,419 @@ func TestValidateClaimMappings(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
in api.ClaimMappings
|
||||
structuredAuthnFeatureEnabled bool
|
||||
want string
|
||||
wantCELMapper bool
|
||||
}{
|
||||
{
|
||||
name: "username claim is empty",
|
||||
in: api.ClaimMappings{Username: api.PrefixedClaimOrExpression{Claim: "", Prefix: pointer.String("prefix")}},
|
||||
want: "issuer.claimMappings.username.claim: Required value: claim name is required",
|
||||
},
|
||||
{
|
||||
name: "username prefix is empty",
|
||||
in: api.ClaimMappings{Username: api.PrefixedClaimOrExpression{Claim: "claim"}},
|
||||
want: "issuer.claimMappings.username.prefix: Required value: prefix is required",
|
||||
},
|
||||
{
|
||||
name: "groups prefix is empty",
|
||||
name: "username expression and claim are set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
|
||||
Groups: api.PrefixedClaimOrExpression{Claim: "claim"},
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Expression: "claims.username",
|
||||
},
|
||||
want: "issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set",
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.username: Invalid value: "": claim and expression can't both be set`,
|
||||
},
|
||||
{
|
||||
name: "groups prefix set but claim is empty",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
|
||||
Groups: api.PrefixedClaimOrExpression{Prefix: pointer.String("prefix")},
|
||||
name: "username expression and claim are empty",
|
||||
in: api.ClaimMappings{Username: api.PrefixedClaimOrExpression{}},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: "issuer.claimMappings.username: Required value: claim or expression is required",
|
||||
},
|
||||
want: "issuer.claimMappings.groups.claim: Required value: non-empty claim name is required when prefix is set",
|
||||
{
|
||||
name: "username prefix set when expression is set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.username.prefix: Invalid value: "prefix": prefix can't be set when expression is set`,
|
||||
},
|
||||
{
|
||||
name: "username prefix is nil when claim is set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.username.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`,
|
||||
},
|
||||
{
|
||||
name: "username expression is invalid",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "groups expression and claim are set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.groups: Invalid value: "": claim and expression can't both be set`,
|
||||
},
|
||||
{
|
||||
name: "groups prefix set when expression is set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.groups.prefix: Invalid value: "prefix": prefix can't be set when expression is set`,
|
||||
},
|
||||
{
|
||||
name: "groups prefix is nil when claim is set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`,
|
||||
},
|
||||
{
|
||||
name: "groups expression is invalid",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "uid claim and expression are set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
UID: api.ClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Expression: "claims.uid",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.uid: Invalid value: "": claim and expression can't both be set`,
|
||||
},
|
||||
{
|
||||
name: "uid expression is invalid",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
UID: api.ClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping key is empty",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].key: Required value`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping value expression is empty",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: ""},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].valueExpression: Required value: valueExpression is required`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping value expression is invalid",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: "foo.bar"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].valueExpression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "username expression is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `[issuer.claimMappings.username.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^]`,
|
||||
},
|
||||
{
|
||||
name: "groups expression is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `[issuer.claimMappings.groups.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^]`,
|
||||
},
|
||||
{
|
||||
name: "uid expression is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
UID: api.ClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `[issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^]`,
|
||||
},
|
||||
{
|
||||
name: "uid claim is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
UID: api.ClaimOrExpression{
|
||||
Claim: "claim",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `issuer.claimMappings.extra: Invalid value: "": extra claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
||||
},
|
||||
{
|
||||
name: "duplicate extra mapping key",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
||||
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
||||
{Key: "example.org/foo", ValueExpression: "claims.extras"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[1].key: Duplicate value: "example.org/foo"`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping key is not domain prefix path",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
||||
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "foo", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].key: Invalid value: "foo": must be a domain-prefixed path (such as "acme.io/foo")`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping key is not lower case",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
||||
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/Foo", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].key: Invalid value: "example.org/Foo": key must be lowercase`,
|
||||
},
|
||||
{
|
||||
name: "valid claim mappings",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
|
||||
Groups: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
|
||||
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
||||
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
||||
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
wantCELMapper: true,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := validateClaimMappings(tt.in, fldPath).ToAggregate()
|
||||
celMapper := &authenticationcel.CELMapper{}
|
||||
got := validateClaimMappings(compiler, celMapper, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
||||
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
||||
fmt.Println(errString(got))
|
||||
t.Fatalf("ClaimMappings validation mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
if tt.wantCELMapper {
|
||||
if len(tt.in.Username.Expression) > 0 && celMapper.Username == nil {
|
||||
t.Fatalf("ClaimMappings validation mismatch: CELMapper.Username is nil")
|
||||
}
|
||||
if len(tt.in.Groups.Expression) > 0 && celMapper.Groups == nil {
|
||||
t.Fatalf("ClaimMappings validation mismatch: CELMapper.Groups is nil")
|
||||
}
|
||||
if len(tt.in.UID.Expression) > 0 && celMapper.UID == nil {
|
||||
t.Fatalf("ClaimMappings validation mismatch: CELMapper.UID is nil")
|
||||
}
|
||||
if len(tt.in.Extra) > 0 && celMapper.Extra == nil {
|
||||
t.Fatalf("ClaimMappings validation mismatch: CELMapper.Extra is nil")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUserValidationRules(t *testing.T) {
|
||||
fldPath := field.NewPath("issuer", "userValidationRules")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
in []api.UserValidationRule
|
||||
structuredAuthnFeatureEnabled bool
|
||||
want string
|
||||
wantCELMapper bool
|
||||
}{
|
||||
{
|
||||
name: "user info validation rule, expression is empty",
|
||||
in: []api.UserValidationRule{{}},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: "issuer.userValidationRules[0].expression: Required value: expression is required",
|
||||
},
|
||||
{
|
||||
name: "duplicate expression",
|
||||
in: []api.UserValidationRule{
|
||||
{Expression: "user.username == 'foo'"},
|
||||
{Expression: "user.username == 'foo'"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`,
|
||||
},
|
||||
{
|
||||
name: "user validation rule is invalid when structured authn feature is disabled",
|
||||
in: []api.UserValidationRule{
|
||||
{Expression: "user.username == 'foo'"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `issuer.userValidationRules: Invalid value: "": user validation rules are not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
||||
},
|
||||
{
|
||||
name: "expression is invalid",
|
||||
in: []api.UserValidationRule{
|
||||
{Expression: "foo.bar"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.userValidationRules[0].expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "expression does not return bool",
|
||||
in: []api.UserValidationRule{
|
||||
{Expression: "user.username"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.userValidationRules[0].expression: Invalid value: "user.username": must evaluate to bool`,
|
||||
},
|
||||
{
|
||||
name: "valid user info validation rule",
|
||||
in: []api.UserValidationRule{
|
||||
{Expression: "user.username == 'foo'"},
|
||||
{Expression: "!user.username.startsWith('system:')", Message: "username cannot used reserved system: prefix"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: "",
|
||||
wantCELMapper: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
celMapper := &authenticationcel.CELMapper{}
|
||||
got := validateUserValidationRules(compiler, celMapper, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
||||
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
||||
t.Fatalf("UserValidationRules validation mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
if tt.wantCELMapper && celMapper.UserValidationRules == nil {
|
||||
t.Fatalf("UserValidationRules validation mismatch: CELMapper.UserValidationRules is nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,12 @@ func (in *ClaimMappings) DeepCopyInto(out *ClaimMappings) {
|
||||
*out = *in
|
||||
in.Username.DeepCopyInto(&out.Username)
|
||||
in.Groups.DeepCopyInto(&out.Groups)
|
||||
out.UID = in.UID
|
||||
if in.Extra != nil {
|
||||
in, out := &in.Extra, &out.Extra
|
||||
*out = make([]ExtraMapping, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,6 +187,22 @@ func (in *ClaimMappings) DeepCopy() *ClaimMappings {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClaimOrExpression) DeepCopyInto(out *ClaimOrExpression) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimOrExpression.
|
||||
func (in *ClaimOrExpression) DeepCopy() *ClaimOrExpression {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ClaimOrExpression)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ClaimValidationRule) DeepCopyInto(out *ClaimValidationRule) {
|
||||
*out = *in
|
||||
@@ -267,6 +289,22 @@ func (in *EgressSelectorConfiguration) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ExtraMapping) DeepCopyInto(out *ExtraMapping) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraMapping.
|
||||
func (in *ExtraMapping) DeepCopy() *ExtraMapping {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ExtraMapping)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Issuer) DeepCopyInto(out *Issuer) {
|
||||
*out = *in
|
||||
@@ -298,6 +336,11 @@ func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) {
|
||||
copy(*out, *in)
|
||||
}
|
||||
in.ClaimMappings.DeepCopyInto(&out.ClaimMappings)
|
||||
if in.UserValidationRules != nil {
|
||||
in, out := &in.UserValidationRules, &out.UserValidationRules
|
||||
*out = make([]UserValidationRule, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -437,6 +480,22 @@ func (in *UDSTransport) DeepCopy() *UDSTransport {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UserValidationRule) DeepCopyInto(out *UserValidationRule) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserValidationRule.
|
||||
func (in *UserValidationRule) DeepCopy() *UserValidationRule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UserValidationRule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *WebhookConfiguration) DeepCopyInto(out *WebhookConfiguration) {
|
||||
*out = *in
|
||||
|
||||
154
staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go
Normal file
154
staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
const (
|
||||
claimsVarName = "claims"
|
||||
userVarName = "user"
|
||||
)
|
||||
|
||||
// compiler implements the Compiler interface.
|
||||
type compiler struct {
|
||||
// varEnvs is a map of CEL environments, keyed by the name of the CEL variable.
|
||||
// The CEL variable is available to the expression.
|
||||
// We have 2 environments, one for claims and one for user.
|
||||
varEnvs map[string]*environment.EnvSet
|
||||
}
|
||||
|
||||
// NewCompiler returns a new Compiler.
|
||||
func NewCompiler(env *environment.EnvSet) Compiler {
|
||||
return &compiler{
|
||||
varEnvs: mustBuildEnvs(env),
|
||||
}
|
||||
}
|
||||
|
||||
// CompileClaimsExpression compiles the given expressionAccessor into a CEL program that can be evaluated.
|
||||
// The claims CEL variable is available to the expression.
|
||||
func (c compiler) CompileClaimsExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error) {
|
||||
return c.compile(expressionAccessor, claimsVarName)
|
||||
}
|
||||
|
||||
// CompileUserExpression compiles the given expressionAccessor into a CEL program that can be evaluated.
|
||||
// The user CEL variable is available to the expression.
|
||||
func (c compiler) CompileUserExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error) {
|
||||
return c.compile(expressionAccessor, userVarName)
|
||||
}
|
||||
|
||||
func (c compiler) compile(expressionAccessor ExpressionAccessor, envVarName string) (CompilationResult, error) {
|
||||
resultError := func(errorString string, errType apiservercel.ErrorType) (CompilationResult, error) {
|
||||
return CompilationResult{}, &apiservercel.Error{
|
||||
Type: errType,
|
||||
Detail: errorString,
|
||||
}
|
||||
}
|
||||
|
||||
env, err := c.varEnvs[envVarName].Env(environment.StoredExpressions)
|
||||
if err != nil {
|
||||
return resultError(fmt.Sprintf("unexpected error loading CEL environment: %v", err), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
|
||||
ast, issues := env.Compile(expressionAccessor.GetExpression())
|
||||
if issues != nil {
|
||||
return resultError("compilation failed: "+issues.String(), apiservercel.ErrorTypeInvalid)
|
||||
}
|
||||
|
||||
found := false
|
||||
returnTypes := expressionAccessor.ReturnTypes()
|
||||
for _, returnType := range returnTypes {
|
||||
if ast.OutputType() == returnType || cel.AnyType == returnType {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
var reason string
|
||||
if len(returnTypes) == 1 {
|
||||
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
|
||||
} else {
|
||||
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
|
||||
}
|
||||
|
||||
return resultError(reason, apiservercel.ErrorTypeInvalid)
|
||||
}
|
||||
|
||||
if _, err = cel.AstToCheckedExpr(ast); err != nil {
|
||||
// should be impossible since env.Compile returned no issues
|
||||
return resultError("unexpected compilation error: "+err.Error(), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
prog, err := env.Program(ast)
|
||||
if err != nil {
|
||||
return resultError("program instantiation failed: "+err.Error(), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
|
||||
return CompilationResult{
|
||||
Program: prog,
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildUserType() *apiservercel.DeclType {
|
||||
field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
|
||||
return apiservercel.NewDeclField(name, declType, required, nil, nil)
|
||||
}
|
||||
fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField {
|
||||
result := make(map[string]*apiservercel.DeclField, len(fields))
|
||||
for _, f := range fields {
|
||||
result[f.Name] = f
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return apiservercel.NewObjectType("kubernetes.UserInfo", fields(
|
||||
field("username", apiservercel.StringType, false),
|
||||
field("uid", apiservercel.StringType, false),
|
||||
field("groups", apiservercel.NewListType(apiservercel.StringType, -1), false),
|
||||
field("extra", apiservercel.NewMapType(apiservercel.StringType, apiservercel.NewListType(apiservercel.StringType, -1), -1), false),
|
||||
))
|
||||
}
|
||||
|
||||
func mustBuildEnvs(baseEnv *environment.EnvSet) map[string]*environment.EnvSet {
|
||||
buildEnvSet := func(envOpts []cel.EnvOption, declTypes []*apiservercel.DeclType) *environment.EnvSet {
|
||||
env, err := baseEnv.Extend(environment.VersionedOptions{
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: envOpts,
|
||||
DeclTypes: declTypes,
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("environment misconfigured: %v", err))
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
userType := buildUserType()
|
||||
claimsType := apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, -1)
|
||||
|
||||
envs := make(map[string]*environment.EnvSet, 2) // build two environments, one for claims and one for user
|
||||
envs[claimsVarName] = buildEnvSet([]cel.EnvOption{cel.Variable(claimsVarName, claimsType.CelType())}, []*apiservercel.DeclType{claimsType})
|
||||
envs[userVarName] = buildEnvSet([]cel.EnvOption{cel.Variable(userVarName, userType.CelType())}, []*apiservercel.DeclType{userType})
|
||||
|
||||
return envs
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
func TestCompileClaimsExpression(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressionAccessors []ExpressionAccessor
|
||||
}{
|
||||
{
|
||||
name: "valid ClaimMappingCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimMappingExpression{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid ClaimValidationCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimValidationCondition{
|
||||
Expression: "claims.foo == 'bar'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid ExtraMapppingCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ExtraMappingExpression{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expressionAccessor := range tc.expressionAccessors {
|
||||
_, err := compiler.CompileClaimsExpression(expressionAccessor)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileUserExpression(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressionAccessors []ExpressionAccessor
|
||||
}{
|
||||
{
|
||||
name: "valid UserValidationCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ExtraMappingExpression{
|
||||
Expression: "user.username == 'bar'",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expressionAccessor := range tc.expressionAccessors {
|
||||
_, err := compiler.CompileUserExpression(expressionAccessor)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileClaimsExpressionError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressionAccessors []ExpressionAccessor
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid ClaimValidationCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimValidationCondition{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
wantErr: "must evaluate to bool",
|
||||
},
|
||||
{
|
||||
name: "UserValidationCondition with wrong env",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&UserValidationCondition{
|
||||
Expression: "user.username == 'foo'",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:1: undeclared reference to 'user' (in container '')`,
|
||||
},
|
||||
{
|
||||
name: "invalid ClaimMappingCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimMappingExpression{
|
||||
Expression: "claims + 1",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:8: found no matching overload for '_+_' applied to '(map(string, any), int)'`,
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expressionAccessor := range tc.expressionAccessors {
|
||||
_, err := compiler.CompileClaimsExpression(expressionAccessor)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("expected error to contain %q but got %q", tc.wantErr, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileUserExpressionError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressionAccessors []ExpressionAccessor
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid UserValidationCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&UserValidationCondition{
|
||||
Expression: "user.username",
|
||||
},
|
||||
},
|
||||
wantErr: "must evaluate to bool",
|
||||
},
|
||||
{
|
||||
name: "ClamMappingCondition with wrong env",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimMappingExpression{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:1: undeclared reference to 'claims' (in container '')`,
|
||||
},
|
||||
{
|
||||
name: "ExtraMappingCondition with wrong env",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ExtraMappingExpression{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:1: undeclared reference to 'claims' (in container '')`,
|
||||
},
|
||||
{
|
||||
name: "ClaimValidationCondition with wrong env",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimValidationCondition{
|
||||
Expression: "claims.foo == 'bar'",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:1: undeclared reference to 'claims' (in container '')`,
|
||||
},
|
||||
{
|
||||
name: "UserValidationCondition expression with unknown field",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&UserValidationCondition{
|
||||
Expression: "user.unknown == 'foo'",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:5: undefined field 'unknown'`,
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expressionAccessor := range tc.expressionAccessors {
|
||||
_, err := compiler.CompileUserExpression(expressionAccessor)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("expected error to contain %q but got %q", tc.wantErr, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUserType(t *testing.T) {
|
||||
userDeclType := buildUserType()
|
||||
userType := reflect.TypeOf(authenticationv1.UserInfo{})
|
||||
|
||||
if len(userDeclType.Fields) != userType.NumField() {
|
||||
t.Errorf("expected %d fields, got %d", userType.NumField(), len(userDeclType.Fields))
|
||||
}
|
||||
|
||||
for i := 0; i < userType.NumField(); i++ {
|
||||
field := userType.Field(i)
|
||||
jsonTagParts := strings.Split(field.Tag.Get("json"), ",")
|
||||
if len(jsonTagParts) < 1 {
|
||||
t.Fatal("expected json tag to be present")
|
||||
}
|
||||
fieldName := jsonTagParts[0]
|
||||
|
||||
declField, ok := userDeclType.Fields[fieldName]
|
||||
if !ok {
|
||||
t.Errorf("expected field %q to be present", field.Name)
|
||||
}
|
||||
if nativeTypeToCELType(t, field.Type).CelType().Equal(declField.Type.CelType()).Value() != true {
|
||||
t.Errorf("expected field %q to have type %v, got %v", field.Name, field.Type, declField.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nativeTypeToCELType(t *testing.T, nativeType reflect.Type) *apiservercel.DeclType {
|
||||
switch nativeType {
|
||||
case reflect.TypeOf(""):
|
||||
return apiservercel.StringType
|
||||
case reflect.TypeOf([]string{}):
|
||||
return apiservercel.NewListType(apiservercel.StringType, -1)
|
||||
case reflect.TypeOf(map[string]authenticationv1.ExtraValue{}):
|
||||
return apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, -1)
|
||||
default:
|
||||
t.Fatalf("unsupported type %v", nativeType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
147
staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go
Normal file
147
staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package cel contains the CEL related interfaces and structs for authentication.
|
||||
package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// ExpressionAccessor is an interface that provides access to a CEL expression.
|
||||
type ExpressionAccessor interface {
|
||||
GetExpression() string
|
||||
ReturnTypes() []*celgo.Type
|
||||
}
|
||||
|
||||
// CompilationResult represents a compiled validations expression.
|
||||
type CompilationResult struct {
|
||||
Program celgo.Program
|
||||
ExpressionAccessor ExpressionAccessor
|
||||
}
|
||||
|
||||
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
|
||||
type EvaluationResult struct {
|
||||
EvalResult ref.Val
|
||||
ExpressionAccessor ExpressionAccessor
|
||||
}
|
||||
|
||||
// Compiler provides a CEL expression compiler configured with the desired authentication related CEL variables.
|
||||
type Compiler interface {
|
||||
CompileClaimsExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error)
|
||||
CompileUserExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error)
|
||||
}
|
||||
|
||||
// ClaimsMapper provides a CEL expression mapper configured with the claims CEL variable.
|
||||
type ClaimsMapper interface {
|
||||
// EvalClaimMapping evaluates the given claim mapping expression and returns a EvaluationResult.
|
||||
// This is used for username, groups and uid claim mapping that contains a single expression.
|
||||
EvalClaimMapping(ctx context.Context, claims *unstructured.Unstructured) (EvaluationResult, error)
|
||||
// EvalClaimMappings evaluates the given expressions and returns a list of EvaluationResult.
|
||||
// This is used for extra claim mapping and claim validation that contains a list of expressions.
|
||||
EvalClaimMappings(ctx context.Context, claims *unstructured.Unstructured) ([]EvaluationResult, error)
|
||||
}
|
||||
|
||||
// UserMapper provides a CEL expression mapper configured with the user CEL variable.
|
||||
type UserMapper interface {
|
||||
// EvalUser evaluates the given user expressions and returns a list of EvaluationResult.
|
||||
// This is used for user validation that contains a list of expressions.
|
||||
EvalUser(ctx context.Context, userInfo *unstructured.Unstructured) ([]EvaluationResult, error)
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &ClaimMappingExpression{}
|
||||
|
||||
// ClaimMappingExpression is a CEL expression that maps a claim.
|
||||
type ClaimMappingExpression struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
// GetExpression returns the CEL expression.
|
||||
func (v *ClaimMappingExpression) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// ReturnTypes returns the CEL expression return types.
|
||||
func (v *ClaimMappingExpression) ReturnTypes() []*celgo.Type {
|
||||
// return types is only used for validation. The claims variable that's available
|
||||
// to the claim mapping expressions is a map[string]interface{}, so we can't
|
||||
// really know what the return type is during compilation. Strict type checking
|
||||
// is done during evaluation.
|
||||
return []*celgo.Type{celgo.AnyType}
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &ClaimValidationCondition{}
|
||||
|
||||
// ClaimValidationCondition is a CEL expression that validates a claim.
|
||||
type ClaimValidationCondition struct {
|
||||
Expression string
|
||||
Message string
|
||||
}
|
||||
|
||||
// GetExpression returns the CEL expression.
|
||||
func (v *ClaimValidationCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// ReturnTypes returns the CEL expression return types.
|
||||
func (v *ClaimValidationCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.BoolType}
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &ExtraMappingExpression{}
|
||||
|
||||
// ExtraMappingExpression is a CEL expression that maps an extra to a list of values.
|
||||
type ExtraMappingExpression struct {
|
||||
Key string
|
||||
Expression string
|
||||
}
|
||||
|
||||
// GetExpression returns the CEL expression.
|
||||
func (v *ExtraMappingExpression) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// ReturnTypes returns the CEL expression return types.
|
||||
func (v *ExtraMappingExpression) ReturnTypes() []*celgo.Type {
|
||||
// return types is only used for validation. The claims variable that's available
|
||||
// to the claim mapping expressions is a map[string]interface{}, so we can't
|
||||
// really know what the return type is during compilation. Strict type checking
|
||||
// is done during evaluation.
|
||||
return []*celgo.Type{celgo.AnyType}
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &UserValidationCondition{}
|
||||
|
||||
// UserValidationCondition is a CEL expression that validates a User.
|
||||
type UserValidationCondition struct {
|
||||
Expression string
|
||||
Message string
|
||||
}
|
||||
|
||||
// GetExpression returns the CEL expression.
|
||||
func (v *UserValidationCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// ReturnTypes returns the CEL expression return types.
|
||||
func (v *UserValidationCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.BoolType}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
var _ ClaimsMapper = &mapper{}
|
||||
var _ UserMapper = &mapper{}
|
||||
|
||||
// mapper implements the ClaimsMapper and UserMapper interface.
|
||||
type mapper struct {
|
||||
compilationResults []CompilationResult
|
||||
}
|
||||
|
||||
// CELMapper is a struct that holds the compiled expressions for
|
||||
// username, groups, uid, extra, claimValidation and userValidation
|
||||
type CELMapper struct {
|
||||
Username ClaimsMapper
|
||||
Groups ClaimsMapper
|
||||
UID ClaimsMapper
|
||||
Extra ClaimsMapper
|
||||
ClaimValidationRules ClaimsMapper
|
||||
UserValidationRules UserMapper
|
||||
}
|
||||
|
||||
// NewClaimsMapper returns a new ClaimsMapper.
|
||||
func NewClaimsMapper(compilationResults []CompilationResult) ClaimsMapper {
|
||||
return &mapper{
|
||||
compilationResults: compilationResults,
|
||||
}
|
||||
}
|
||||
|
||||
// NewUserMapper returns a new UserMapper.
|
||||
func NewUserMapper(compilationResults []CompilationResult) UserMapper {
|
||||
return &mapper{
|
||||
compilationResults: compilationResults,
|
||||
}
|
||||
}
|
||||
|
||||
// EvalClaimMapping evaluates the given claim mapping expression and returns a EvaluationResult.
|
||||
func (m *mapper) EvalClaimMapping(ctx context.Context, claims *unstructured.Unstructured) (EvaluationResult, error) {
|
||||
results, err := m.eval(ctx, map[string]interface{}{claimsVarName: claims.Object})
|
||||
if err != nil {
|
||||
return EvaluationResult{}, err
|
||||
}
|
||||
if len(results) != 1 {
|
||||
return EvaluationResult{}, fmt.Errorf("expected 1 evaluation result, got %d", len(results))
|
||||
}
|
||||
return results[0], nil
|
||||
}
|
||||
|
||||
// EvalClaimMappings evaluates the given expressions and returns a list of EvaluationResult.
|
||||
func (m *mapper) EvalClaimMappings(ctx context.Context, claims *unstructured.Unstructured) ([]EvaluationResult, error) {
|
||||
return m.eval(ctx, map[string]interface{}{claimsVarName: claims.Object})
|
||||
}
|
||||
|
||||
// EvalUser evaluates the given user expressions and returns a list of EvaluationResult.
|
||||
func (m *mapper) EvalUser(ctx context.Context, userInfo *unstructured.Unstructured) ([]EvaluationResult, error) {
|
||||
return m.eval(ctx, map[string]interface{}{userVarName: userInfo.Object})
|
||||
}
|
||||
|
||||
func (m *mapper) eval(ctx context.Context, input map[string]interface{}) ([]EvaluationResult, error) {
|
||||
evaluations := make([]EvaluationResult, len(m.compilationResults))
|
||||
|
||||
for i, compilationResult := range m.compilationResults {
|
||||
var evaluation = &evaluations[i]
|
||||
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
|
||||
|
||||
evalResult, _, err := compilationResult.Program.ContextEval(ctx, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("expression '%s' resulted in error: %w", compilationResult.ExpressionAccessor.GetExpression(), err)
|
||||
}
|
||||
|
||||
evaluation.EvalResult = evalResult
|
||||
}
|
||||
|
||||
return evaluations, nil
|
||||
}
|
||||
@@ -35,18 +35,25 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
"k8s.io/klog/v2"
|
||||
@@ -165,6 +172,13 @@ type Authenticator struct {
|
||||
|
||||
// resolver is used to resolve distributed claims.
|
||||
resolver *claimResolver
|
||||
|
||||
// celMapper contains the compiled CEL expressions for
|
||||
// username, groups, uid, extra, claimMapping and claimValidation
|
||||
celMapper authenticationcel.CELMapper
|
||||
|
||||
// requiredClaims contains the list of claims that must be present in the token.
|
||||
requiredClaims map[string]string
|
||||
}
|
||||
|
||||
func (a *Authenticator) setVerifier(v *oidc.IDTokenVerifier) {
|
||||
@@ -197,7 +211,8 @@ var allowedSigningAlgs = map[string]bool{
|
||||
}
|
||||
|
||||
func New(opts Options) (*Authenticator, error) {
|
||||
if err := apiservervalidation.ValidateJWTAuthenticator(opts.JWTAuthenticator).ToAggregate(); err != nil {
|
||||
celMapper, fieldErr := apiservervalidation.CompileAndValidateJWTAuthenticator(opts.JWTAuthenticator)
|
||||
if err := fieldErr.ToAggregate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -262,10 +277,19 @@ func New(opts Options) (*Authenticator, error) {
|
||||
resolver = newClaimResolver(groupsClaim, client, verifierConfig)
|
||||
}
|
||||
|
||||
requiredClaims := make(map[string]string)
|
||||
for _, claimValidationRule := range opts.JWTAuthenticator.ClaimValidationRules {
|
||||
if len(claimValidationRule.Claim) > 0 {
|
||||
requiredClaims[claimValidationRule.Claim] = claimValidationRule.RequiredValue
|
||||
}
|
||||
}
|
||||
|
||||
authenticator := &Authenticator{
|
||||
jwtAuthenticator: opts.JWTAuthenticator,
|
||||
cancel: cancel,
|
||||
resolver: resolver,
|
||||
celMapper: celMapper,
|
||||
requiredClaims: requiredClaims,
|
||||
}
|
||||
|
||||
if opts.KeySet != nil {
|
||||
@@ -521,10 +545,130 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
|
||||
}
|
||||
}
|
||||
|
||||
var claimsUnstructured *unstructured.Unstructured
|
||||
// Convert the claims to unstructured so that we can evaluate the CEL expressions
|
||||
// against the claims. This is done once here so that we don't have to convert
|
||||
// the claims to unstructured multiple times in the CEL mapper for each mapping.
|
||||
// Only perform this conversion if any of the mapping or validation rules contain
|
||||
// CEL expressions.
|
||||
// TODO(aramase): In the future when we look into making distributed claims work,
|
||||
// we should see if we can skip this function and use a dynamic type resolver for
|
||||
// both json.RawMessage and the distributed claim fetching.
|
||||
if a.celMapper.Username != nil || a.celMapper.Groups != nil || a.celMapper.UID != nil || a.celMapper.Extra != nil || a.celMapper.ClaimValidationRules != nil {
|
||||
if claimsUnstructured, err = convertObjectToUnstructured(&c); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: could not convert claims to unstructured: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var username string
|
||||
if username, err = a.getUsername(ctx, c, claimsUnstructured); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
info := &user.DefaultInfo{Name: username}
|
||||
if info.Groups, err = a.getGroups(ctx, c, claimsUnstructured); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if info.UID, err = a.getUID(ctx, c, claimsUnstructured); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
extra, err := a.getExtra(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
info.Extra = extra
|
||||
}
|
||||
|
||||
// check to ensure all required claims are present in the ID token and have matching values.
|
||||
for claim, value := range a.requiredClaims {
|
||||
if !c.hasClaim(claim) {
|
||||
return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim)
|
||||
}
|
||||
|
||||
// NOTE: Only string values are supported as valid required claim values.
|
||||
var claimValue string
|
||||
if err := c.unmarshalClaim(claim, &claimValue); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse claim %s: %w", claim, err)
|
||||
}
|
||||
if claimValue != value {
|
||||
return nil, false, fmt.Errorf("oidc: required claim %s value does not match. Got = %s, want = %s", claim, claimValue, value)
|
||||
}
|
||||
}
|
||||
|
||||
if a.celMapper.ClaimValidationRules != nil {
|
||||
evalResult, err := a.celMapper.ClaimValidationRules.EvalClaimMappings(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: error evaluating claim validation expression: %w", err)
|
||||
}
|
||||
if err := checkValidationRulesEvaluation(evalResult, func(a authenticationcel.ExpressionAccessor) (string, error) {
|
||||
claimValidationCondition, ok := a.(*authenticationcel.ClaimValidationCondition)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid type conversion, expected ClaimValidationCondition")
|
||||
}
|
||||
return claimValidationCondition.Message, nil
|
||||
}); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: error evaluating claim validation expression: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if a.celMapper.UserValidationRules != nil {
|
||||
userInfo := &authenticationv1.UserInfo{
|
||||
Extra: make(map[string]authenticationv1.ExtraValue),
|
||||
Groups: info.GetGroups(),
|
||||
UID: info.GetUID(),
|
||||
Username: info.GetName(),
|
||||
}
|
||||
// Convert the extra information in the user object
|
||||
for key, val := range info.GetExtra() {
|
||||
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
|
||||
}
|
||||
|
||||
// Convert the user info to unstructured so that we can evaluate the CEL expressions
|
||||
// against the user info. This is done once here so that we don't have to convert
|
||||
// the user info to unstructured multiple times in the CEL mapper for each mapping.
|
||||
userInfoUnstructured, err := convertObjectToUnstructured(userInfo)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: could not convert user info to unstructured: %v", err)
|
||||
}
|
||||
|
||||
evalResult, err := a.celMapper.UserValidationRules.EvalUser(ctx, userInfoUnstructured)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: error evaluating user info validation rule: %w", err)
|
||||
}
|
||||
if err := checkValidationRulesEvaluation(evalResult, func(a authenticationcel.ExpressionAccessor) (string, error) {
|
||||
userValidationCondition, ok := a.(*authenticationcel.UserValidationCondition)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid type conversion, expected UserValidationCondition")
|
||||
}
|
||||
return userValidationCondition.Message, nil
|
||||
}); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: error evaluating user info validation rule: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &authenticator.Response{User: info}, true, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) getUsername(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) (string, error) {
|
||||
if a.celMapper.Username != nil {
|
||||
evalResult, err := a.celMapper.Username.EvalClaimMapping(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", err)
|
||||
}
|
||||
if evalResult.EvalResult.Type() != celgo.StringType {
|
||||
return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", fmt.Errorf("username claim expression must return a string"))
|
||||
}
|
||||
|
||||
return evalResult.EvalResult.Value().(string), nil
|
||||
}
|
||||
|
||||
var username string
|
||||
usernameClaim := a.jwtAuthenticator.ClaimMappings.Username.Claim
|
||||
if err := c.unmarshalClaim(usernameClaim, &username); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", usernameClaim, err)
|
||||
return "", fmt.Errorf("oidc: parse username claims %q: %v", usernameClaim, err)
|
||||
}
|
||||
|
||||
if usernameClaim == "email" {
|
||||
@@ -533,24 +677,26 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
|
||||
if hasEmailVerified := c.hasClaim("email_verified"); hasEmailVerified {
|
||||
var emailVerified bool
|
||||
if err := c.unmarshalClaim("email_verified", &emailVerified); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse 'email_verified' claim: %v", err)
|
||||
return "", fmt.Errorf("oidc: parse 'email_verified' claim: %v", err)
|
||||
}
|
||||
|
||||
// If the email_verified claim is present we have to verify it is set to `true`.
|
||||
if !emailVerified {
|
||||
return nil, false, fmt.Errorf("oidc: email not verified")
|
||||
return "", fmt.Errorf("oidc: email not verified")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userNamePrefix := a.jwtAuthenticator.ClaimMappings.Username.Prefix
|
||||
if userNamePrefix != nil && *userNamePrefix != "" {
|
||||
username = *userNamePrefix + username
|
||||
return *userNamePrefix + username, nil
|
||||
}
|
||||
return username, nil
|
||||
}
|
||||
|
||||
info := &user.DefaultInfo{Name: username}
|
||||
func (a *Authenticator) getGroups(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) ([]string, error) {
|
||||
groupsClaim := a.jwtAuthenticator.ClaimMappings.Groups.Claim
|
||||
if groupsClaim != "" {
|
||||
if len(groupsClaim) > 0 {
|
||||
if _, ok := c[groupsClaim]; ok {
|
||||
// Some admins want to use string claims like "role" as the group value.
|
||||
// Allow the group claim to be a single string instead of an array.
|
||||
@@ -558,39 +704,91 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
|
||||
// See: https://github.com/kubernetes/kubernetes/issues/33290
|
||||
var groups stringOrArray
|
||||
if err := c.unmarshalClaim(groupsClaim, &groups); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", groupsClaim, err)
|
||||
return nil, fmt.Errorf("oidc: parse groups claim %q: %w", groupsClaim, err)
|
||||
}
|
||||
info.Groups = []string(groups)
|
||||
|
||||
prefix := a.jwtAuthenticator.ClaimMappings.Groups.Prefix
|
||||
if prefix != nil && *prefix != "" {
|
||||
for i, group := range groups {
|
||||
groups[i] = *prefix + group
|
||||
}
|
||||
}
|
||||
|
||||
groupsPrefix := a.jwtAuthenticator.ClaimMappings.Groups.Prefix
|
||||
if groupsPrefix != nil && *groupsPrefix != "" {
|
||||
for i, group := range info.Groups {
|
||||
info.Groups[i] = *groupsPrefix + group
|
||||
return []string(groups), nil
|
||||
}
|
||||
}
|
||||
|
||||
// check to ensure all required claims are present in the ID token and have matching values.
|
||||
for _, claimValidationRule := range a.jwtAuthenticator.ClaimValidationRules {
|
||||
claim := claimValidationRule.Claim
|
||||
value := claimValidationRule.RequiredValue
|
||||
|
||||
if !c.hasClaim(claim) {
|
||||
return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim)
|
||||
if a.celMapper.Groups == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// NOTE: Only string values are supported as valid required claim values.
|
||||
var claimValue string
|
||||
if err := c.unmarshalClaim(claim, &claimValue); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse claim %s: %v", claim, err)
|
||||
}
|
||||
if claimValue != value {
|
||||
return nil, false, fmt.Errorf("oidc: required claim %s value does not match. Got = %s, want = %s", claim, claimValue, value)
|
||||
}
|
||||
evalResult, err := a.celMapper.Groups.EvalClaimMapping(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: error evaluating group claim expression: %w", err)
|
||||
}
|
||||
|
||||
return &authenticator.Response{User: info}, true, nil
|
||||
groups, err := convertCELValueToStringList(evalResult.EvalResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: error evaluating group claim expression: %w", err)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) getUID(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) (string, error) {
|
||||
uidClaim := a.jwtAuthenticator.ClaimMappings.UID.Claim
|
||||
if len(uidClaim) > 0 {
|
||||
var uid string
|
||||
if err := c.unmarshalClaim(uidClaim, &uid); err != nil {
|
||||
return "", fmt.Errorf("oidc: parse uid claim %q: %w", uidClaim, err)
|
||||
}
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
if a.celMapper.UID == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
evalResult, err := a.celMapper.UID.EvalClaimMapping(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oidc: error evaluating uid claim expression: %w", err)
|
||||
}
|
||||
if evalResult.EvalResult.Type() != celgo.StringType {
|
||||
return "", fmt.Errorf("oidc: error evaluating uid claim expression: %w", fmt.Errorf("uid claim expression must return a string"))
|
||||
}
|
||||
|
||||
return evalResult.EvalResult.Value().(string), nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) getExtra(ctx context.Context, claimsUnstructured *unstructured.Unstructured) (map[string][]string, error) {
|
||||
if a.celMapper.Extra == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
evalResult, err := a.celMapper.Extra.EvalClaimMappings(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := make(map[string][]string, len(evalResult))
|
||||
for _, result := range evalResult {
|
||||
extraMapping, ok := result.ExpressionAccessor.(*authenticationcel.ExtraMappingExpression)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("oidc: error evaluating extra claim expression: %w", fmt.Errorf("invalid type conversion, expected ExtraMappingCondition"))
|
||||
}
|
||||
|
||||
extraValues, err := convertCELValueToStringList(result.EvalResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: error evaluating extra claim expression: %s: %w", extraMapping.Expression, err)
|
||||
}
|
||||
|
||||
if len(extraValues) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
extra[extraMapping.Key] = extraValues
|
||||
}
|
||||
|
||||
return extra, nil
|
||||
}
|
||||
|
||||
// getClaimJWT gets a distributed claim JWT from url, using the supplied access
|
||||
@@ -655,3 +853,94 @@ func (c claims) hasClaim(name string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// convertCELValueToStringList converts the CEL value to a string list.
|
||||
// The CEL value needs to be either a string or a list of strings.
|
||||
// "", [] are treated as not being present and will return nil.
|
||||
// Empty string in a list of strings is treated as not being present and will be filtered out.
|
||||
func convertCELValueToStringList(val ref.Val) ([]string, error) {
|
||||
switch val.Type().TypeName() {
|
||||
case celgo.StringType.TypeName():
|
||||
out := val.Value().(string)
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return []string{out}, nil
|
||||
|
||||
case celgo.ListType(nil).TypeName():
|
||||
var result []string
|
||||
switch val.Value().(type) {
|
||||
case []interface{}:
|
||||
for _, v := range val.Value().([]interface{}) {
|
||||
out, ok := v.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expression must return a string or a list of strings")
|
||||
}
|
||||
if len(out) == 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, out)
|
||||
}
|
||||
case []ref.Val:
|
||||
for _, v := range val.Value().([]ref.Val) {
|
||||
out, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expression must return a string or a list of strings")
|
||||
}
|
||||
if len(out) == 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, out)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("expression must return a string or a list of strings")
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
case celgo.NullType.TypeName():
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("expression must return a string or a list of strings")
|
||||
}
|
||||
}
|
||||
|
||||
// messageFunc is a function that returns a message for a validation rule.
|
||||
type messageFunc func(authenticationcel.ExpressionAccessor) (string, error)
|
||||
|
||||
// checkValidationRulesEvaluation checks if the validation rules evaluation results
|
||||
// are valid. If the validation rules evaluation results are not valid, it returns
|
||||
// an error with an optional message that was set in the validation rule.
|
||||
func checkValidationRulesEvaluation(results []authenticationcel.EvaluationResult, messageFn messageFunc) error {
|
||||
for _, result := range results {
|
||||
if result.EvalResult.Type() != celgo.BoolType {
|
||||
return fmt.Errorf("validation expression must return a boolean")
|
||||
}
|
||||
if !result.EvalResult.Value().(bool) {
|
||||
expression := result.ExpressionAccessor.GetExpression()
|
||||
|
||||
message, err := messageFn(result.ExpressionAccessor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("validation expression '%s' failed: %s", expression, message)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
|
||||
if obj == nil || reflect.ValueOf(obj).IsNil() {
|
||||
return &unstructured.Unstructured{Object: nil}, nil
|
||||
}
|
||||
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &unstructured.Unstructured{Object: ret}, nil
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ import (
|
||||
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
@@ -288,6 +291,11 @@ func (c *claimsTest) run(t *testing.T) {
|
||||
t.Fatalf("wanted initialization error %q but got none", c.wantInitErr)
|
||||
}
|
||||
|
||||
claims := struct{}{}
|
||||
if err := json.Unmarshal([]byte(c.claims), &claims); err != nil {
|
||||
t.Fatalf("failed to unmarshal claims: %v", err)
|
||||
}
|
||||
|
||||
// Sign and serialize the claims in a JWT.
|
||||
jws, err := signer.Sign([]byte(c.claims))
|
||||
if err != nil {
|
||||
@@ -333,6 +341,8 @@ func (c *claimsTest) run(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
|
||||
|
||||
synchronizeTokenIDVerifierForTest = true
|
||||
tests := []claimsTest{
|
||||
{
|
||||
@@ -1801,7 +1811,7 @@ func TestToken(t *testing.T) {
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
wantInitErr: `claimMappings.username.claim: Required value: claim name is required`,
|
||||
wantInitErr: `claimMappings.username: Required value: claim or expression is required`,
|
||||
},
|
||||
{
|
||||
name: "invalid-sig-alg",
|
||||
@@ -1910,6 +1920,813 @@ func TestToken(t *testing.T) {
|
||||
}`, valid.Unix()),
|
||||
wantErr: `oidc: verify token: oidc: expected audience "my-client" got ["my-wrong-client"]`,
|
||||
},
|
||||
{
|
||||
name: "user validation rule fails for user.username",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: "username",
|
||||
Prefix: pointer.String("system:"),
|
||||
},
|
||||
},
|
||||
UserValidationRules: []apiserver.UserValidationRule{
|
||||
{
|
||||
Expression: "!user.username.startsWith('system:')",
|
||||
Message: "username cannot used reserved system: prefix",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
wantErr: `oidc: error evaluating user info validation rule: validation expression '!user.username.startsWith('system:')' failed: username cannot used reserved system: prefix`,
|
||||
},
|
||||
{
|
||||
name: "user validation rule fails for user.groups",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: "groups",
|
||||
Prefix: pointer.String("system:"),
|
||||
},
|
||||
},
|
||||
UserValidationRules: []apiserver.UserValidationRule{
|
||||
{
|
||||
Expression: "user.groups.all(group, !group.startsWith('system:'))",
|
||||
Message: "groups cannot used reserved system: prefix",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d,
|
||||
"groups": ["team1", "team2"]
|
||||
}`, valid.Unix()),
|
||||
wantErr: `oidc: error evaluating user info validation rule: validation expression 'user.groups.all(group, !group.startsWith('system:'))' failed: groups cannot used reserved system: prefix`,
|
||||
},
|
||||
{
|
||||
name: "claim validation rule with expression fails",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: "username",
|
||||
Prefix: pointer.String(""),
|
||||
},
|
||||
},
|
||||
ClaimValidationRules: []apiserver.ClaimValidationRule{
|
||||
{
|
||||
Expression: `claims.hd == "example.com"`,
|
||||
Message: "hd claim must be example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
wantErr: `oidc: error evaluating claim validation expression: expression 'claims.hd == "example.com"' resulted in error: no such key: hd`,
|
||||
},
|
||||
{
|
||||
name: "claim validation rule with expression",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: "username",
|
||||
Prefix: pointer.String(""),
|
||||
},
|
||||
},
|
||||
ClaimValidationRules: []apiserver.ClaimValidationRule{
|
||||
{
|
||||
Expression: `claims.hd == "example.com"`,
|
||||
Message: "hd claim must be example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d,
|
||||
"hd": "example.com"
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claim validation rule with expression and nested claims",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: "username",
|
||||
Prefix: pointer.String(""),
|
||||
},
|
||||
},
|
||||
ClaimValidationRules: []apiserver.ClaimValidationRule{
|
||||
{
|
||||
Expression: `claims.foo.bar == "baz"`,
|
||||
Message: "foo.bar claim must be baz",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d,
|
||||
"hd": "example.com",
|
||||
"foo": {
|
||||
"bar": "baz"
|
||||
}
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claim validation rule with mix of expression and claim",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: "username",
|
||||
Prefix: pointer.String(""),
|
||||
},
|
||||
},
|
||||
ClaimValidationRules: []apiserver.ClaimValidationRule{
|
||||
{
|
||||
Expression: `claims.foo.bar == "baz"`,
|
||||
Message: "foo.bar claim must be baz",
|
||||
},
|
||||
{
|
||||
Claim: "hd",
|
||||
RequiredValue: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d,
|
||||
"hd": "example.com",
|
||||
"foo": {
|
||||
"bar": "baz"
|
||||
}
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "username claim mapping with expression",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "username claim mapping with expression and nested claim",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.foo.username",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d,
|
||||
"foo": {
|
||||
"username": "jane"
|
||||
}
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groups claim mapping with expression",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1", "team2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groups claim with expression",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: "username",
|
||||
Prefix: pointer.String("oidc:"),
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: `(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "groups:" + role)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"roles": "foo,bar",
|
||||
"other_roles": "baz,qux",
|
||||
"exp": %d
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "oidc:jane",
|
||||
Groups: []string{"groups:foo", "groups:bar", "groups:baz", "groups:qux"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uid claim mapping with expression",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
UID: apiserver.ClaimOrExpression{
|
||||
Expression: "claims.uid",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d,
|
||||
"uid": "1234"
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1", "team2"},
|
||||
UID: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uid claim mapping with claim",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
UID: apiserver.ClaimOrExpression{
|
||||
Claim: "uid",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d,
|
||||
"uid": "1234"
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1", "team2"},
|
||||
UID: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extra claim mapping with expression",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
UID: apiserver.ClaimOrExpression{
|
||||
Expression: "claims.uid",
|
||||
},
|
||||
Extra: []apiserver.ExtraMapping{
|
||||
{
|
||||
Key: "example.org/foo",
|
||||
ValueExpression: "claims.foo",
|
||||
},
|
||||
{
|
||||
Key: "example.org/bar",
|
||||
ValueExpression: "claims.bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d,
|
||||
"uid": "1234",
|
||||
"foo": "bar",
|
||||
"bar": [
|
||||
"baz",
|
||||
"qux"
|
||||
]
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1", "team2"},
|
||||
UID: "1234",
|
||||
Extra: map[string][]string{
|
||||
"example.org/foo": {"bar"},
|
||||
"example.org/bar": {"baz", "qux"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extra claim mapping, value derived from claim value",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Extra: []apiserver.ExtraMapping{
|
||||
{
|
||||
Key: "example.org/admin",
|
||||
ValueExpression: `(has(claims.is_admin) && claims.is_admin) ? "true":""`,
|
||||
},
|
||||
{
|
||||
Key: "example.org/admin_1",
|
||||
ValueExpression: `claims.?is_admin.orValue(false) == true ? "true":""`,
|
||||
},
|
||||
{
|
||||
Key: "example.org/non_existent",
|
||||
ValueExpression: `claims.?non_existent.orValue("default") == "default" ? "true":""`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d,
|
||||
"is_admin": true
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Extra: map[string][]string{
|
||||
"example.org/admin": {"true"},
|
||||
"example.org/admin_1": {"true"},
|
||||
"example.org/non_existent": {"true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hardcoded extra claim mapping",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Extra: []apiserver.ExtraMapping{
|
||||
{
|
||||
Key: "example.org/admin",
|
||||
ValueExpression: `"true"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"exp": %d,
|
||||
"is_admin": true
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Extra: map[string][]string{
|
||||
"example.org/admin": {"true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extra claim mapping, multiple expressions for same key",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
UID: apiserver.ClaimOrExpression{
|
||||
Expression: "claims.uid",
|
||||
},
|
||||
Extra: []apiserver.ExtraMapping{
|
||||
{
|
||||
Key: "example.org/foo",
|
||||
ValueExpression: "claims.foo",
|
||||
},
|
||||
{
|
||||
Key: "example.org/bar",
|
||||
ValueExpression: "claims.bar",
|
||||
},
|
||||
{
|
||||
Key: "example.org/foo",
|
||||
ValueExpression: "claims.bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d,
|
||||
"uid": "1234",
|
||||
"foo": "bar",
|
||||
"bar": [
|
||||
"baz",
|
||||
"qux"
|
||||
]
|
||||
}`, valid.Unix()),
|
||||
wantInitErr: `claimMappings.extra[2].key: Duplicate value: "example.org/foo"`,
|
||||
},
|
||||
{
|
||||
name: "extra claim mapping, empty string value for key",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
UID: apiserver.ClaimOrExpression{
|
||||
Expression: "claims.uid",
|
||||
},
|
||||
Extra: []apiserver.ExtraMapping{
|
||||
{
|
||||
Key: "example.org/foo",
|
||||
ValueExpression: "claims.foo",
|
||||
},
|
||||
{
|
||||
Key: "example.org/bar",
|
||||
ValueExpression: "claims.bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d,
|
||||
"uid": "1234",
|
||||
"foo": "",
|
||||
"bar": [
|
||||
"baz",
|
||||
"qux"
|
||||
]
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1", "team2"},
|
||||
UID: "1234",
|
||||
Extra: map[string][]string{
|
||||
"example.org/bar": {"baz", "qux"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extra claim mapping with user validation rule succeeds",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
UID: apiserver.ClaimOrExpression{
|
||||
Expression: "claims.uid",
|
||||
},
|
||||
Extra: []apiserver.ExtraMapping{
|
||||
{
|
||||
Key: "example.org/foo",
|
||||
ValueExpression: "'bar'",
|
||||
},
|
||||
{
|
||||
Key: "example.org/baz",
|
||||
ValueExpression: "claims.baz",
|
||||
},
|
||||
},
|
||||
},
|
||||
UserValidationRules: []apiserver.UserValidationRule{
|
||||
{
|
||||
Expression: "'bar' in user.extra['example.org/foo'] && 'qux' in user.extra['example.org/baz']",
|
||||
Message: "example.org/foo must be bar and example.org/baz must be qux",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": ["team1", "team2"],
|
||||
"exp": %d,
|
||||
"uid": "1234",
|
||||
"baz": "qux"
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
Groups: []string{"team1", "team2"},
|
||||
UID: "1234",
|
||||
Extra: map[string][]string{
|
||||
"example.org/foo": {"bar"},
|
||||
"example.org/baz": {"qux"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groups expression returns null",
|
||||
options: Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: "https://auth.example.com",
|
||||
Audiences: []string{"my-client"},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.username",
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
},
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
},
|
||||
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
pubKeys: []*jose.JSONWebKey{
|
||||
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||
},
|
||||
claims: fmt.Sprintf(`{
|
||||
"iss": "https://auth.example.com",
|
||||
"aud": "my-client",
|
||||
"username": "jane",
|
||||
"groups": null,
|
||||
"exp": %d,
|
||||
"uid": "1234",
|
||||
"baz": "qux"
|
||||
}`, valid.Unix()),
|
||||
want: &user.DefaultInfo{
|
||||
Name: "jane",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -99,6 +100,9 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// authenticationConfigFunc is a function that returns a string representation of an authentication config.
|
||||
type authenticationConfigFunc func(t *testing.T, issuerURL, caCert string) string
|
||||
|
||||
func TestOIDC(t *testing.T) {
|
||||
t.Log("Testing OIDC authenticator with --oidc-* flags")
|
||||
runTests(t, false)
|
||||
@@ -114,7 +118,7 @@ func TestStructuredAuthenticationConfig(t *testing.T) {
|
||||
func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
configureInfrastructure func(t *testing.T, useAuthenticationConfig bool) (
|
||||
configureInfrastructure func(t *testing.T, fn authenticationConfigFunc) (
|
||||
oidcServer *utilsoidc.TestServer,
|
||||
apiServer *kubeapiserverapptesting.TestServer,
|
||||
signingPrivateKey *rsa.PrivateKey,
|
||||
@@ -129,27 +133,29 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
certPath,
|
||||
oidcServerURL,
|
||||
oidcServerTokenURL string,
|
||||
) *kubernetes.Clientset
|
||||
asserErrFn func(t *testing.T, errorToCheck error)
|
||||
) kubernetes.Interface
|
||||
assertErrFn func(t *testing.T, errorToCheck error)
|
||||
}{
|
||||
{
|
||||
name: "ID token is ok",
|
||||
configureInfrastructure: configureTestInfrastructure,
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
idTokenLifetime := time.Second * 1200
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviourReturningPredefinedJWT(
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
oidcServer.URL(),
|
||||
defaultOIDCClientID,
|
||||
defaultOIDCClaimedUsername,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(idTokenLifetime).Unix(),
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
time.Now().Add(idTokenLifetime).Unix(),
|
||||
))
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
asserErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.NoError(t, errorToCheck)
|
||||
},
|
||||
},
|
||||
@@ -160,7 +166,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
configureOIDCServerToReturnExpiredIDToken(t, 2, oidcServer, signingPrivateKey)
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
asserErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
|
||||
},
|
||||
},
|
||||
@@ -171,7 +177,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrBadClientID)
|
||||
},
|
||||
configureClient: configureClientWithEmptyIDToken,
|
||||
asserErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
urlError, ok := errorToCheck.(*url.Error)
|
||||
require.True(t, ok)
|
||||
assert.Equal(
|
||||
@@ -185,7 +191,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
name: "client has wrong CA",
|
||||
configureInfrastructure: configureTestInfrastructure,
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {},
|
||||
configureClient: func(t *testing.T, restCfg *rest.Config, caCert []byte, _, oidcServerURL, oidcServerTokenURL string) *kubernetes.Clientset {
|
||||
configureClient: func(t *testing.T, restCfg *rest.Config, caCert []byte, _, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface {
|
||||
tempDir := t.TempDir()
|
||||
certFilePath := filepath.Join(tempDir, "localhost_127.0.0.1_.crt")
|
||||
|
||||
@@ -194,7 +200,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
|
||||
return configureClientWithEmptyIDToken(t, restCfg, caCert, certFilePath, oidcServerURL, oidcServerTokenURL)
|
||||
},
|
||||
asserErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
expectedErr := new(x509.UnknownAuthorityError)
|
||||
assert.ErrorAs(t, errorToCheck, expectedErr)
|
||||
},
|
||||
@@ -212,7 +218,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
}, nil)
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
asserErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
expectedError := new(apierrors.StatusError)
|
||||
assert.ErrorAs(t, errorToCheck, &expectedError)
|
||||
assert.Equal(
|
||||
@@ -224,7 +230,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
},
|
||||
{
|
||||
name: "ID token signature can not be verified due to wrong JWKs",
|
||||
configureInfrastructure: func(t *testing.T, useAuthenticationConfig bool) (
|
||||
configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc) (
|
||||
oidcServer *utilsoidc.TestServer,
|
||||
apiServer *kubeapiserverapptesting.TestServer,
|
||||
signingPrivateKey *rsa.PrivateKey,
|
||||
@@ -239,7 +245,21 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
|
||||
|
||||
if useAuthenticationConfig {
|
||||
authenticationConfig := generateAuthenticationConfig(t, oidcServer.URL(), defaultOIDCClientID, string(caCertContent), defaultOIDCUsernamePrefix)
|
||||
authenticationConfig := fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
claim: sub
|
||||
prefix: %s
|
||||
`, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)), defaultOIDCUsernamePrefix)
|
||||
apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig)
|
||||
} else {
|
||||
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "")
|
||||
@@ -250,24 +270,26 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
|
||||
anotherSigningPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
|
||||
require.NoError(t, wantErr)
|
||||
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehaviour(t, &anotherSigningPrivateKey.PublicKey))
|
||||
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &anotherSigningPrivateKey.PublicKey))
|
||||
|
||||
return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
|
||||
},
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviourReturningPredefinedJWT(
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
oidcServer.URL(),
|
||||
defaultOIDCClientID,
|
||||
defaultOIDCClaimedUsername,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(time.Second * 1200).Unix(),
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
time.Now().Add(time.Second*1200).Unix(),
|
||||
))
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
asserErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
|
||||
},
|
||||
},
|
||||
@@ -275,7 +297,27 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, useAuthenticationConfig)
|
||||
fn := func(t *testing.T, issuerURL, caCert string) string { return "" }
|
||||
if useAuthenticationConfig {
|
||||
fn = func(t *testing.T, issuerURL, caCert string) string {
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
claim: sub
|
||||
prefix: %s
|
||||
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert), defaultOIDCUsernamePrefix)
|
||||
}
|
||||
}
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, fn)
|
||||
|
||||
tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
|
||||
|
||||
@@ -284,12 +326,10 @@ func runTests(t *testing.T, useAuthenticationConfig bool) {
|
||||
|
||||
client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
ctx := testContext(t)
|
||||
_, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
|
||||
|
||||
tt.asserErrFn(t, err)
|
||||
tt.assertErrFn(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -298,24 +338,26 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
configureUpdatingTokenBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey)
|
||||
asserErrFn func(t *testing.T, errorToCheck error)
|
||||
assertErrFn func(t *testing.T, errorToCheck error)
|
||||
}{
|
||||
{
|
||||
name: "cache returns stale client if refresh token is not updated in config",
|
||||
configureUpdatingTokenBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviourReturningPredefinedJWT(
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
oidcServer.URL(),
|
||||
defaultOIDCClientID,
|
||||
defaultOIDCClaimedUsername,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(time.Second * 1200).Unix(),
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
time.Now().Add(time.Second*1200).Unix(),
|
||||
))
|
||||
configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer)
|
||||
},
|
||||
asserErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
urlError, ok := errorToCheck.(*url.Error)
|
||||
require.True(t, ok)
|
||||
assert.Equal(
|
||||
@@ -327,7 +369,7 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, false)
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, func(t *testing.T, _, _ string) string { return "" })
|
||||
|
||||
tokenURL, err := oidcServer.TokenURL()
|
||||
require.NoError(t, err)
|
||||
@@ -339,9 +381,7 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
|
||||
expiredClient := kubernetes.NewForConfigOrDie(clientConfig)
|
||||
configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
ctx := testContext(t)
|
||||
_, err = expiredClient.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
|
||||
assert.Error(t, err)
|
||||
|
||||
@@ -351,12 +391,347 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
|
||||
expectedOkClient := kubernetes.NewForConfigOrDie(clientConfig)
|
||||
_, err = expectedOkClient.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
|
||||
|
||||
tt.asserErrFn(t, err)
|
||||
tt.assertErrFn(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func configureTestInfrastructure(t *testing.T, useAuthenticationConfig bool) (
|
||||
func TestStructuredAuthenticationConfigCEL(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
authConfigFn authenticationConfigFunc
|
||||
configureInfrastructure func(t *testing.T, fn authenticationConfigFunc) (
|
||||
oidcServer *utilsoidc.TestServer,
|
||||
apiServer *kubeapiserverapptesting.TestServer,
|
||||
signingPrivateKey *rsa.PrivateKey,
|
||||
caCertContent []byte,
|
||||
caFilePath string,
|
||||
)
|
||||
configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey)
|
||||
configureClient func(
|
||||
t *testing.T,
|
||||
restCfg *rest.Config,
|
||||
caCert []byte,
|
||||
certPath,
|
||||
oidcServerURL,
|
||||
oidcServerTokenURL string,
|
||||
) kubernetes.Interface
|
||||
assertErrFn func(t *testing.T, errorToCheck error)
|
||||
wantUser *authenticationv1.UserInfo
|
||||
}{
|
||||
{
|
||||
name: "username CEL expression is ok",
|
||||
authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
expression: "'k8s-' + claims.sub"
|
||||
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
|
||||
},
|
||||
configureInfrastructure: configureTestInfrastructure,
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
idTokenLifetime := time.Second * 1200
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(idTokenLifetime).Unix(),
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
))
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.NoError(t, errorToCheck)
|
||||
},
|
||||
wantUser: &authenticationv1.UserInfo{
|
||||
Username: "k8s-john_doe",
|
||||
Groups: []string{"system:authenticated"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "groups CEL expression is ok",
|
||||
authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
expression: "'k8s-' + claims.sub"
|
||||
groups:
|
||||
expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "prefix:" + role)'
|
||||
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
|
||||
},
|
||||
configureInfrastructure: configureTestInfrastructure,
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
idTokenLifetime := time.Second * 1200
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(idTokenLifetime).Unix(),
|
||||
"roles": "foo,bar",
|
||||
"other_roles": "baz,qux",
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
))
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.NoError(t, errorToCheck)
|
||||
},
|
||||
wantUser: &authenticationv1.UserInfo{
|
||||
Username: "k8s-john_doe",
|
||||
Groups: []string{"prefix:foo", "prefix:bar", "prefix:baz", "prefix:qux", "system:authenticated"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claim validation rule fails",
|
||||
authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
expression: "'k8s-' + claims.sub"
|
||||
claimValidationRules:
|
||||
- expression: 'claims.hd == "example.com"'
|
||||
message: "the hd claim must be set to example.com"
|
||||
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
|
||||
},
|
||||
configureInfrastructure: configureTestInfrastructure,
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
idTokenLifetime := time.Second * 1200
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(idTokenLifetime).Unix(),
|
||||
"hd": "notexample.com",
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
))
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extra mapping CEL expressions are ok",
|
||||
authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
expression: "'k8s-' + claims.sub"
|
||||
extra:
|
||||
- key: "example.org/foo"
|
||||
valueExpression: "'bar'"
|
||||
- key: "example.org/baz"
|
||||
valueExpression: "claims.baz"
|
||||
userValidationRules:
|
||||
- expression: "'bar' in user.extra['example.org/foo'] && 'qux' in user.extra['example.org/baz']"
|
||||
message: "example.org/foo must be bar and example.org/baz must be qux"
|
||||
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
|
||||
},
|
||||
configureInfrastructure: configureTestInfrastructure,
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
idTokenLifetime := time.Second * 1200
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(idTokenLifetime).Unix(),
|
||||
"baz": "qux",
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
))
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.NoError(t, errorToCheck)
|
||||
},
|
||||
wantUser: &authenticationv1.UserInfo{
|
||||
Username: "k8s-john_doe",
|
||||
Groups: []string{"system:authenticated"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"example.org/foo": {"bar"},
|
||||
"example.org/baz": {"qux"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uid CEL expression is ok",
|
||||
authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
expression: "'k8s-' + claims.sub"
|
||||
uid:
|
||||
expression: "claims.uid"
|
||||
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
|
||||
},
|
||||
configureInfrastructure: configureTestInfrastructure,
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
idTokenLifetime := time.Second * 1200
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(idTokenLifetime).Unix(),
|
||||
"uid": "1234",
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
))
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.NoError(t, errorToCheck)
|
||||
},
|
||||
wantUser: &authenticationv1.UserInfo{
|
||||
Username: "k8s-john_doe",
|
||||
Groups: []string{"system:authenticated"},
|
||||
UID: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user validation rule fails",
|
||||
authConfigFn: func(t *testing.T, issuerURL, caCert string) string {
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
expression: "'k8s-' + claims.sub"
|
||||
groups:
|
||||
expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "system:" + role)'
|
||||
userValidationRules:
|
||||
- expression: "user.groups.all(group, !group.startsWith('system:'))"
|
||||
message: "groups cannot used reserved system: prefix"
|
||||
`, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert))
|
||||
},
|
||||
configureInfrastructure: configureTestInfrastructure,
|
||||
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
|
||||
idTokenLifetime := time.Second * 1200
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(idTokenLifetime).Unix(),
|
||||
"roles": "foo,bar",
|
||||
"other_roles": "baz,qux",
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
))
|
||||
},
|
||||
configureClient: configureClientFetchingOIDCCredentials,
|
||||
assertErrFn: func(t *testing.T, errorToCheck error) {
|
||||
assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
|
||||
},
|
||||
wantUser: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, tt.authConfigFn)
|
||||
|
||||
tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
|
||||
|
||||
tokenURL, err := oidcServer.TokenURL()
|
||||
require.NoError(t, err)
|
||||
|
||||
client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
|
||||
|
||||
ctx := testContext(t)
|
||||
|
||||
if tt.wantUser != nil {
|
||||
res, err := client.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, *tt.wantUser, res.Status.UserInfo)
|
||||
}
|
||||
|
||||
_, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
|
||||
tt.assertErrFn(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func configureTestInfrastructure(t *testing.T, fn authenticationConfigFunc) (
|
||||
oidcServer *utilsoidc.TestServer,
|
||||
apiServer *kubeapiserverapptesting.TestServer,
|
||||
signingPrivateKey *rsa.PrivateKey,
|
||||
@@ -372,14 +747,14 @@ func configureTestInfrastructure(t *testing.T, useAuthenticationConfig bool) (
|
||||
|
||||
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
|
||||
|
||||
if useAuthenticationConfig {
|
||||
authenticationConfig := generateAuthenticationConfig(t, oidcServer.URL(), defaultOIDCClientID, string(caCertContent), defaultOIDCUsernamePrefix)
|
||||
authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent))
|
||||
if len(authenticationConfig) > 0 {
|
||||
apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig)
|
||||
} else {
|
||||
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "")
|
||||
}
|
||||
|
||||
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehaviour(t, &signingPrivateKey.PublicKey))
|
||||
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &signingPrivateKey.PublicKey))
|
||||
|
||||
adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
|
||||
configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
|
||||
@@ -387,19 +762,19 @@ func configureTestInfrastructure(t *testing.T, useAuthenticationConfig bool) (
|
||||
return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
|
||||
}
|
||||
|
||||
func configureClientFetchingOIDCCredentials(t *testing.T, restCfg *rest.Config, caCert []byte, certPath, oidcServerURL, oidcServerTokenURL string) *kubernetes.Clientset {
|
||||
func configureClientFetchingOIDCCredentials(t *testing.T, restCfg *rest.Config, caCert []byte, certPath, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface {
|
||||
idToken, stubRefreshToken := fetchOIDCCredentials(t, oidcServerTokenURL, caCert)
|
||||
clientConfig := configureClientConfigForOIDC(t, restCfg, defaultOIDCClientID, certPath, idToken, stubRefreshToken, oidcServerURL)
|
||||
return kubernetes.NewForConfigOrDie(clientConfig)
|
||||
}
|
||||
|
||||
func configureClientWithEmptyIDToken(t *testing.T, restCfg *rest.Config, _ []byte, certPath, oidcServerURL, _ string) *kubernetes.Clientset {
|
||||
func configureClientWithEmptyIDToken(t *testing.T, restCfg *rest.Config, _ []byte, certPath, oidcServerURL, _ string) kubernetes.Interface {
|
||||
emptyIDToken, stubRefreshToken := "", defaultStubRefreshToken
|
||||
clientConfig := configureClientConfigForOIDC(t, restCfg, defaultOIDCClientID, certPath, emptyIDToken, stubRefreshToken, oidcServerURL)
|
||||
return kubernetes.NewForConfigOrDie(clientConfig)
|
||||
}
|
||||
|
||||
func configureRBAC(t *testing.T, clientset *kubernetes.Clientset, role *rbacv1.Role, binding *rbacv1.RoleBinding) {
|
||||
func configureRBAC(t *testing.T, clientset kubernetes.Interface, role *rbacv1.Role, binding *rbacv1.RoleBinding) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
@@ -500,15 +875,17 @@ func configureOIDCServerToReturnExpiredIDToken(t *testing.T, returningExpiredTok
|
||||
t.Helper()
|
||||
|
||||
oidcServer.TokenHandler().EXPECT().Token().Times(returningExpiredTokenTimes).DoAndReturn(func() (utilsoidc.Token, error) {
|
||||
token, err := utilsoidc.TokenHandlerBehaviourReturningPredefinedJWT(
|
||||
token, err := utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t,
|
||||
signingPrivateKey,
|
||||
oidcServer.URL(),
|
||||
defaultOIDCClientID,
|
||||
defaultOIDCClaimedUsername,
|
||||
map[string]interface{}{
|
||||
"iss": oidcServer.URL(),
|
||||
"sub": defaultOIDCClaimedUsername,
|
||||
"aud": defaultOIDCClientID,
|
||||
"exp": time.Now().Add(-time.Millisecond).Unix(),
|
||||
},
|
||||
defaultStubAccessToken,
|
||||
defaultStubRefreshToken,
|
||||
time.Now().Add(-time.Millisecond).Unix(),
|
||||
)()
|
||||
return token, err
|
||||
})
|
||||
@@ -548,26 +925,14 @@ func writeTempFile(t *testing.T, content string) string {
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
func generateAuthenticationConfig(t *testing.T, issuerURL, clientID, caCert, usernamePrefix string) string {
|
||||
t.Helper()
|
||||
|
||||
// Indent the certificate authority to match the format of the generated
|
||||
// authentication config.
|
||||
caCert = strings.ReplaceAll(caCert, "\n", "\n ")
|
||||
|
||||
return fmt.Sprintf(`
|
||||
apiVersion: apiserver.config.k8s.io/v1alpha1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: %s
|
||||
audiences:
|
||||
- %s
|
||||
certificateAuthority: |
|
||||
%s
|
||||
claimMappings:
|
||||
username:
|
||||
claim: sub
|
||||
prefix: %s
|
||||
`, issuerURL, clientID, string(caCert), usernamePrefix)
|
||||
// indentCertificateAuthority indents the certificate authority to match
|
||||
// the format of the generated authentication config.
|
||||
func indentCertificateAuthority(caCert string) string {
|
||||
return strings.ReplaceAll(caCert, "\n", "\n ")
|
||||
}
|
||||
|
||||
func testContext(t *testing.T) context.Context {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -170,17 +170,12 @@ func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
|
||||
return oidcServer
|
||||
}
|
||||
|
||||
// TokenHandlerBehaviourReturningPredefinedJWT describes the scenario when signed JWT token is being created.
|
||||
// This behaviour should being applied to the MockTokenHandler.
|
||||
func TokenHandlerBehaviourReturningPredefinedJWT(
|
||||
// TokenHandlerBehaviorReturningPredefinedJWT describes the scenario when signed JWT token is being created.
|
||||
// This behavior should being applied to the MockTokenHandler.
|
||||
func TokenHandlerBehaviorReturningPredefinedJWT(
|
||||
t *testing.T,
|
||||
rsaPrivateKey *rsa.PrivateKey,
|
||||
issClaim,
|
||||
audClaim,
|
||||
subClaim,
|
||||
accessToken,
|
||||
refreshToken string,
|
||||
expClaim int64,
|
||||
claims map[string]interface{}, accessToken, refreshToken string,
|
||||
) func() (Token, error) {
|
||||
t.Helper()
|
||||
|
||||
@@ -188,18 +183,7 @@ func TokenHandlerBehaviourReturningPredefinedJWT(
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: rsaPrivateKey}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
payload := struct {
|
||||
Iss string `json:"iss"`
|
||||
Aud string `json:"aud"`
|
||||
Sub string `json:"sub"`
|
||||
Exp int64 `json:"exp"`
|
||||
}{
|
||||
Iss: issClaim,
|
||||
Aud: audClaim,
|
||||
Sub: subClaim,
|
||||
Exp: expClaim,
|
||||
}
|
||||
payloadJSON, err := json.Marshal(payload)
|
||||
payloadJSON, err := json.Marshal(claims)
|
||||
require.NoError(t, err)
|
||||
|
||||
idTokenSignature, err := signer.Sign(payloadJSON)
|
||||
@@ -215,9 +199,9 @@ func TokenHandlerBehaviourReturningPredefinedJWT(
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultJwksHandlerBehaviour describes the scenario when JSON Web Key Set token is being returned.
|
||||
// This behaviour should being applied to the MockJWKsHandler.
|
||||
func DefaultJwksHandlerBehaviour(t *testing.T, verificationPublicKey *rsa.PublicKey) func() jose.JSONWebKeySet {
|
||||
// DefaultJwksHandlerBehavior describes the scenario when JSON Web Key Set token is being returned.
|
||||
// This behavior should being applied to the MockJWKsHandler.
|
||||
func DefaultJwksHandlerBehavior(t *testing.T, verificationPublicKey *rsa.PublicKey) func() jose.JSONWebKeySet {
|
||||
t.Helper()
|
||||
|
||||
return func() jose.JSONWebKeySet {
|
||||
|
||||
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@@ -1501,6 +1501,7 @@ k8s.io/apiserver/pkg/audit
|
||||
k8s.io/apiserver/pkg/audit/policy
|
||||
k8s.io/apiserver/pkg/authentication/authenticator
|
||||
k8s.io/apiserver/pkg/authentication/authenticatorfactory
|
||||
k8s.io/apiserver/pkg/authentication/cel
|
||||
k8s.io/apiserver/pkg/authentication/group
|
||||
k8s.io/apiserver/pkg/authentication/request/anonymous
|
||||
k8s.io/apiserver/pkg/authentication/request/bearertoken
|
||||
|
||||
Reference in New Issue
Block a user