diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go
index ddf0b4b100d..7b22a200f96 100644
--- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go
+++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go
@@ -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"))
}
- if seenClaims.Has(rule.Claim) {
- allErrs = append(allErrs, field.Duplicate(fldPath.Child("claim"), rule.Claim))
- continue
+ 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))
+ }
+ 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)
+ }
}
- seenClaims.Insert(rule.Claim)
+ }
+
+ 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"))
+ }
+ 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.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 len(m.Extra) > 0 {
+ allErrs = append(allErrs, field.Invalid(fldPath.Child("extra"), "", "extra claim mapping 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"))
+
+ 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})
}
- 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"))
+
+ 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})
}
- 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"))
+
+ 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{}
diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go
index 22630309d28..ca688a0bee5 100644
--- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go
+++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go
@@ -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
- want string
+ 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"},
},
- want: `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`,
+ structuredAuthnFeatureEnabled: true,
+ want: `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`,
},
{
- name: "valid claim validation rule",
- in: []api.ClaimValidationRule{{Claim: "claim", RequiredValue: "value"}},
- want: "",
+ 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: :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"},
},
- want: "",
+ 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")
+ }
})
}
}
@@ -363,52 +474,421 @@ func TestValidateClaimMappings(t *testing.T) {
fldPath := field.NewPath("issuer", "claimMappings")
testCases := []struct {
- name string
- in api.ClaimMappings
- want string
+ 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",
+ 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",
+ },
+ {
+ name: "username prefix set when expression is set",
in: api.ClaimMappings{
- Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
- Groups: api.PrefixedClaimOrExpression{Prefix: pointer.String("prefix")},
+ Username: api.PrefixedClaimOrExpression{
+ Expression: "claims.username",
+ Prefix: pointer.String("prefix"),
+ },
},
- want: "issuer.claimMappings.groups.claim: Required value: non-empty claim name is required when prefix is set",
+ 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: :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: :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: :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: :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: :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: :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: :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"},
+ },
},
- want: "",
+ 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: :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")
+ }
})
}
}
diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go
new file mode 100644
index 00000000000..3bcff5e9051
--- /dev/null
+++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go
@@ -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
+}
diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile_test.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile_test.go
new file mode 100644
index 00000000000..c8659aa830b
--- /dev/null
+++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/compile_test.go
@@ -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: :1:1: undeclared reference to 'user' (in container '')`,
+ },
+ {
+ name: "invalid ClaimMappingCondition",
+ expressionAccessors: []ExpressionAccessor{
+ &ClaimMappingExpression{
+ Expression: "claims + 1",
+ },
+ },
+ wantErr: `compilation failed: ERROR: :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: :1:1: undeclared reference to 'claims' (in container '')`,
+ },
+ {
+ name: "ExtraMappingCondition with wrong env",
+ expressionAccessors: []ExpressionAccessor{
+ &ExtraMappingExpression{
+ Expression: "claims.foo",
+ },
+ },
+ wantErr: `compilation failed: ERROR: :1:1: undeclared reference to 'claims' (in container '')`,
+ },
+ {
+ name: "ClaimValidationCondition with wrong env",
+ expressionAccessors: []ExpressionAccessor{
+ &ClaimValidationCondition{
+ Expression: "claims.foo == 'bar'",
+ },
+ },
+ wantErr: `compilation failed: ERROR: :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: :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
+}
diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go
new file mode 100644
index 00000000000..7ec0c9af6af
--- /dev/null
+++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go
@@ -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}
+}
diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go
new file mode 100644
index 00000000000..ab308bb7f0f
--- /dev/null
+++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go
@@ -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
+}
diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go
index 148ae79dfc6..76fc3cdd592 100644
--- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go
+++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go
@@ -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
+ }
+ }
+
+ return []string(groups), nil
}
}
- groupsPrefix := a.jwtAuthenticator.ClaimMappings.Groups.Prefix
- if groupsPrefix != nil && *groupsPrefix != "" {
- for i, group := range info.Groups {
- info.Groups[i] = *groupsPrefix + group
- }
+ if a.celMapper.Groups == nil {
+ return nil, 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)
- }
-
- // 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
+}
diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go
index 692b5cea1a4..4826ced7c5b 100644
--- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go
+++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go
@@ -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)
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 043b2e5bf00..0e823a2d34b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -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