mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-16 23:29:21 +00:00
Implement CEL and wire it with OIDC authenticator
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
parent
6b971153d7
commit
26e3a03d12
@ -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{}
|
||||
|
@ -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: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "expression does not evaluate to bool",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Expression: "claims.foo"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo": must evaluate to bool`,
|
||||
},
|
||||
{
|
||||
name: "valid claim validation rule with expression",
|
||||
in: []api.ClaimValidationRule{
|
||||
{Expression: "claims.foo == 'bar'"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: "",
|
||||
wantCELMapper: true,
|
||||
},
|
||||
{
|
||||
name: "valid claim validation rule with multiple rules",
|
||||
@ -345,16 +451,21 @@ func TestClaimValidationRules(t *testing.T) {
|
||||
{Claim: "claim1", RequiredValue: "value1"},
|
||||
{Claim: "claim2", RequiredValue: "value2"},
|
||||
},
|
||||
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: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "groups expression and claim are set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Expression: "claims.groups",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.groups: Invalid value: "": claim and expression can't both be set`,
|
||||
},
|
||||
{
|
||||
name: "groups prefix set when expression is set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Expression: "claims.groups",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.groups.prefix: Invalid value: "prefix": prefix can't be set when expression is set`,
|
||||
},
|
||||
{
|
||||
name: "groups prefix is nil when claim is set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`,
|
||||
},
|
||||
{
|
||||
name: "groups expression is invalid",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "uid claim and expression are set",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
UID: api.ClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Expression: "claims.uid",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.uid: Invalid value: "": claim and expression can't both be set`,
|
||||
},
|
||||
{
|
||||
name: "uid expression is invalid",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
UID: api.ClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping key is empty",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].key: Required value`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping value expression is empty",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: ""},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].valueExpression: Required value: valueExpression is required`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping value expression is invalid",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: "foo.bar"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].valueExpression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "username expression is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `[issuer.claimMappings.username.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^]`,
|
||||
},
|
||||
{
|
||||
name: "groups expression is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Groups: api.PrefixedClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `[issuer.claimMappings.groups.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^]`,
|
||||
},
|
||||
{
|
||||
name: "uid expression is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
UID: api.ClaimOrExpression{
|
||||
Expression: "foo.bar",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `[issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^]`,
|
||||
},
|
||||
{
|
||||
name: "uid claim is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
UID: api.ClaimOrExpression{
|
||||
Claim: "claim",
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping is invalid when structured authn feature is disabled",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{
|
||||
Claim: "claim",
|
||||
Prefix: pointer.String("prefix"),
|
||||
},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: false,
|
||||
want: `issuer.claimMappings.extra: Invalid value: "": extra claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
||||
},
|
||||
{
|
||||
name: "duplicate extra mapping key",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
||||
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
||||
{Key: "example.org/foo", ValueExpression: "claims.extras"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[1].key: Duplicate value: "example.org/foo"`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping key is not domain prefix path",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
||||
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "foo", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].key: Invalid value: "foo": must be a domain-prefixed path (such as "acme.io/foo")`,
|
||||
},
|
||||
{
|
||||
name: "extra mapping key is not lower case",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
||||
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/Foo", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.claimMappings.extra[0].key: Invalid value: "example.org/Foo": key must be lowercase`,
|
||||
},
|
||||
{
|
||||
name: "valid claim mappings",
|
||||
in: api.ClaimMappings{
|
||||
Username: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
|
||||
Groups: api.PrefixedClaimOrExpression{Claim: "claim", Prefix: pointer.String("prefix")},
|
||||
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
||||
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
||||
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
||||
Extra: []api.ExtraMapping{
|
||||
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
||||
},
|
||||
},
|
||||
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: <input>:1:1: undeclared reference to 'foo' (in container '')
|
||||
| foo.bar
|
||||
| ^`,
|
||||
},
|
||||
{
|
||||
name: "expression does not return bool",
|
||||
in: []api.UserValidationRule{
|
||||
{Expression: "user.username"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: `issuer.userValidationRules[0].expression: Invalid value: "user.username": must evaluate to bool`,
|
||||
},
|
||||
{
|
||||
name: "valid user info validation rule",
|
||||
in: []api.UserValidationRule{
|
||||
{Expression: "user.username == 'foo'"},
|
||||
{Expression: "!user.username.startsWith('system:')", Message: "username cannot used reserved system: prefix"},
|
||||
},
|
||||
structuredAuthnFeatureEnabled: true,
|
||||
want: "",
|
||||
wantCELMapper: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
celMapper := &authenticationcel.CELMapper{}
|
||||
got := validateUserValidationRules(compiler, celMapper, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
||||
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
||||
t.Fatalf("UserValidationRules validation mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
if tt.wantCELMapper && celMapper.UserValidationRules == nil {
|
||||
t.Fatalf("UserValidationRules validation mismatch: CELMapper.UserValidationRules is nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
154
staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go
Normal file
154
staging/src/k8s.io/apiserver/pkg/authentication/cel/compile.go
Normal file
@ -0,0 +1,154 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
const (
|
||||
claimsVarName = "claims"
|
||||
userVarName = "user"
|
||||
)
|
||||
|
||||
// compiler implements the Compiler interface.
|
||||
type compiler struct {
|
||||
// varEnvs is a map of CEL environments, keyed by the name of the CEL variable.
|
||||
// The CEL variable is available to the expression.
|
||||
// We have 2 environments, one for claims and one for user.
|
||||
varEnvs map[string]*environment.EnvSet
|
||||
}
|
||||
|
||||
// NewCompiler returns a new Compiler.
|
||||
func NewCompiler(env *environment.EnvSet) Compiler {
|
||||
return &compiler{
|
||||
varEnvs: mustBuildEnvs(env),
|
||||
}
|
||||
}
|
||||
|
||||
// CompileClaimsExpression compiles the given expressionAccessor into a CEL program that can be evaluated.
|
||||
// The claims CEL variable is available to the expression.
|
||||
func (c compiler) CompileClaimsExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error) {
|
||||
return c.compile(expressionAccessor, claimsVarName)
|
||||
}
|
||||
|
||||
// CompileUserExpression compiles the given expressionAccessor into a CEL program that can be evaluated.
|
||||
// The user CEL variable is available to the expression.
|
||||
func (c compiler) CompileUserExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error) {
|
||||
return c.compile(expressionAccessor, userVarName)
|
||||
}
|
||||
|
||||
func (c compiler) compile(expressionAccessor ExpressionAccessor, envVarName string) (CompilationResult, error) {
|
||||
resultError := func(errorString string, errType apiservercel.ErrorType) (CompilationResult, error) {
|
||||
return CompilationResult{}, &apiservercel.Error{
|
||||
Type: errType,
|
||||
Detail: errorString,
|
||||
}
|
||||
}
|
||||
|
||||
env, err := c.varEnvs[envVarName].Env(environment.StoredExpressions)
|
||||
if err != nil {
|
||||
return resultError(fmt.Sprintf("unexpected error loading CEL environment: %v", err), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
|
||||
ast, issues := env.Compile(expressionAccessor.GetExpression())
|
||||
if issues != nil {
|
||||
return resultError("compilation failed: "+issues.String(), apiservercel.ErrorTypeInvalid)
|
||||
}
|
||||
|
||||
found := false
|
||||
returnTypes := expressionAccessor.ReturnTypes()
|
||||
for _, returnType := range returnTypes {
|
||||
if ast.OutputType() == returnType || cel.AnyType == returnType {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
var reason string
|
||||
if len(returnTypes) == 1 {
|
||||
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
|
||||
} else {
|
||||
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
|
||||
}
|
||||
|
||||
return resultError(reason, apiservercel.ErrorTypeInvalid)
|
||||
}
|
||||
|
||||
if _, err = cel.AstToCheckedExpr(ast); err != nil {
|
||||
// should be impossible since env.Compile returned no issues
|
||||
return resultError("unexpected compilation error: "+err.Error(), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
prog, err := env.Program(ast)
|
||||
if err != nil {
|
||||
return resultError("program instantiation failed: "+err.Error(), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
|
||||
return CompilationResult{
|
||||
Program: prog,
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildUserType() *apiservercel.DeclType {
|
||||
field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
|
||||
return apiservercel.NewDeclField(name, declType, required, nil, nil)
|
||||
}
|
||||
fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField {
|
||||
result := make(map[string]*apiservercel.DeclField, len(fields))
|
||||
for _, f := range fields {
|
||||
result[f.Name] = f
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return apiservercel.NewObjectType("kubernetes.UserInfo", fields(
|
||||
field("username", apiservercel.StringType, false),
|
||||
field("uid", apiservercel.StringType, false),
|
||||
field("groups", apiservercel.NewListType(apiservercel.StringType, -1), false),
|
||||
field("extra", apiservercel.NewMapType(apiservercel.StringType, apiservercel.NewListType(apiservercel.StringType, -1), -1), false),
|
||||
))
|
||||
}
|
||||
|
||||
func mustBuildEnvs(baseEnv *environment.EnvSet) map[string]*environment.EnvSet {
|
||||
buildEnvSet := func(envOpts []cel.EnvOption, declTypes []*apiservercel.DeclType) *environment.EnvSet {
|
||||
env, err := baseEnv.Extend(environment.VersionedOptions{
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: envOpts,
|
||||
DeclTypes: declTypes,
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("environment misconfigured: %v", err))
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
userType := buildUserType()
|
||||
claimsType := apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, -1)
|
||||
|
||||
envs := make(map[string]*environment.EnvSet, 2) // build two environments, one for claims and one for user
|
||||
envs[claimsVarName] = buildEnvSet([]cel.EnvOption{cel.Variable(claimsVarName, claimsType.CelType())}, []*apiservercel.DeclType{claimsType})
|
||||
envs[userVarName] = buildEnvSet([]cel.EnvOption{cel.Variable(userVarName, userType.CelType())}, []*apiservercel.DeclType{userType})
|
||||
|
||||
return envs
|
||||
}
|
@ -0,0 +1,263 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
func TestCompileClaimsExpression(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressionAccessors []ExpressionAccessor
|
||||
}{
|
||||
{
|
||||
name: "valid ClaimMappingCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimMappingExpression{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid ClaimValidationCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimValidationCondition{
|
||||
Expression: "claims.foo == 'bar'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid ExtraMapppingCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ExtraMappingExpression{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expressionAccessor := range tc.expressionAccessors {
|
||||
_, err := compiler.CompileClaimsExpression(expressionAccessor)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileUserExpression(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressionAccessors []ExpressionAccessor
|
||||
}{
|
||||
{
|
||||
name: "valid UserValidationCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ExtraMappingExpression{
|
||||
Expression: "user.username == 'bar'",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expressionAccessor := range tc.expressionAccessors {
|
||||
_, err := compiler.CompileUserExpression(expressionAccessor)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileClaimsExpressionError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressionAccessors []ExpressionAccessor
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid ClaimValidationCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimValidationCondition{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
wantErr: "must evaluate to bool",
|
||||
},
|
||||
{
|
||||
name: "UserValidationCondition with wrong env",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&UserValidationCondition{
|
||||
Expression: "user.username == 'foo'",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:1: undeclared reference to 'user' (in container '')`,
|
||||
},
|
||||
{
|
||||
name: "invalid ClaimMappingCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimMappingExpression{
|
||||
Expression: "claims + 1",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:8: found no matching overload for '_+_' applied to '(map(string, any), int)'`,
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expressionAccessor := range tc.expressionAccessors {
|
||||
_, err := compiler.CompileClaimsExpression(expressionAccessor)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("expected error to contain %q but got %q", tc.wantErr, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileUserExpressionError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressionAccessors []ExpressionAccessor
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid UserValidationCondition",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&UserValidationCondition{
|
||||
Expression: "user.username",
|
||||
},
|
||||
},
|
||||
wantErr: "must evaluate to bool",
|
||||
},
|
||||
{
|
||||
name: "ClamMappingCondition with wrong env",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimMappingExpression{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:1: undeclared reference to 'claims' (in container '')`,
|
||||
},
|
||||
{
|
||||
name: "ExtraMappingCondition with wrong env",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ExtraMappingExpression{
|
||||
Expression: "claims.foo",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:1: undeclared reference to 'claims' (in container '')`,
|
||||
},
|
||||
{
|
||||
name: "ClaimValidationCondition with wrong env",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&ClaimValidationCondition{
|
||||
Expression: "claims.foo == 'bar'",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:1: undeclared reference to 'claims' (in container '')`,
|
||||
},
|
||||
{
|
||||
name: "UserValidationCondition expression with unknown field",
|
||||
expressionAccessors: []ExpressionAccessor{
|
||||
&UserValidationCondition{
|
||||
Expression: "user.unknown == 'foo'",
|
||||
},
|
||||
},
|
||||
wantErr: `compilation failed: ERROR: <input>:1:5: undefined field 'unknown'`,
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expressionAccessor := range tc.expressionAccessors {
|
||||
_, err := compiler.CompileUserExpression(expressionAccessor)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("expected error to contain %q but got %q", tc.wantErr, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUserType(t *testing.T) {
|
||||
userDeclType := buildUserType()
|
||||
userType := reflect.TypeOf(authenticationv1.UserInfo{})
|
||||
|
||||
if len(userDeclType.Fields) != userType.NumField() {
|
||||
t.Errorf("expected %d fields, got %d", userType.NumField(), len(userDeclType.Fields))
|
||||
}
|
||||
|
||||
for i := 0; i < userType.NumField(); i++ {
|
||||
field := userType.Field(i)
|
||||
jsonTagParts := strings.Split(field.Tag.Get("json"), ",")
|
||||
if len(jsonTagParts) < 1 {
|
||||
t.Fatal("expected json tag to be present")
|
||||
}
|
||||
fieldName := jsonTagParts[0]
|
||||
|
||||
declField, ok := userDeclType.Fields[fieldName]
|
||||
if !ok {
|
||||
t.Errorf("expected field %q to be present", field.Name)
|
||||
}
|
||||
if nativeTypeToCELType(t, field.Type).CelType().Equal(declField.Type.CelType()).Value() != true {
|
||||
t.Errorf("expected field %q to have type %v, got %v", field.Name, field.Type, declField.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nativeTypeToCELType(t *testing.T, nativeType reflect.Type) *apiservercel.DeclType {
|
||||
switch nativeType {
|
||||
case reflect.TypeOf(""):
|
||||
return apiservercel.StringType
|
||||
case reflect.TypeOf([]string{}):
|
||||
return apiservercel.NewListType(apiservercel.StringType, -1)
|
||||
case reflect.TypeOf(map[string]authenticationv1.ExtraValue{}):
|
||||
return apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, -1)
|
||||
default:
|
||||
t.Fatalf("unsupported type %v", nativeType)
|
||||
}
|
||||
return nil
|
||||
}
|
147
staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go
Normal file
147
staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package cel contains the CEL related interfaces and structs for authentication.
|
||||
package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// ExpressionAccessor is an interface that provides access to a CEL expression.
|
||||
type ExpressionAccessor interface {
|
||||
GetExpression() string
|
||||
ReturnTypes() []*celgo.Type
|
||||
}
|
||||
|
||||
// CompilationResult represents a compiled validations expression.
|
||||
type CompilationResult struct {
|
||||
Program celgo.Program
|
||||
ExpressionAccessor ExpressionAccessor
|
||||
}
|
||||
|
||||
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
|
||||
type EvaluationResult struct {
|
||||
EvalResult ref.Val
|
||||
ExpressionAccessor ExpressionAccessor
|
||||
}
|
||||
|
||||
// Compiler provides a CEL expression compiler configured with the desired authentication related CEL variables.
|
||||
type Compiler interface {
|
||||
CompileClaimsExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error)
|
||||
CompileUserExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error)
|
||||
}
|
||||
|
||||
// ClaimsMapper provides a CEL expression mapper configured with the claims CEL variable.
|
||||
type ClaimsMapper interface {
|
||||
// EvalClaimMapping evaluates the given claim mapping expression and returns a EvaluationResult.
|
||||
// This is used for username, groups and uid claim mapping that contains a single expression.
|
||||
EvalClaimMapping(ctx context.Context, claims *unstructured.Unstructured) (EvaluationResult, error)
|
||||
// EvalClaimMappings evaluates the given expressions and returns a list of EvaluationResult.
|
||||
// This is used for extra claim mapping and claim validation that contains a list of expressions.
|
||||
EvalClaimMappings(ctx context.Context, claims *unstructured.Unstructured) ([]EvaluationResult, error)
|
||||
}
|
||||
|
||||
// UserMapper provides a CEL expression mapper configured with the user CEL variable.
|
||||
type UserMapper interface {
|
||||
// EvalUser evaluates the given user expressions and returns a list of EvaluationResult.
|
||||
// This is used for user validation that contains a list of expressions.
|
||||
EvalUser(ctx context.Context, userInfo *unstructured.Unstructured) ([]EvaluationResult, error)
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &ClaimMappingExpression{}
|
||||
|
||||
// ClaimMappingExpression is a CEL expression that maps a claim.
|
||||
type ClaimMappingExpression struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
// GetExpression returns the CEL expression.
|
||||
func (v *ClaimMappingExpression) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// ReturnTypes returns the CEL expression return types.
|
||||
func (v *ClaimMappingExpression) ReturnTypes() []*celgo.Type {
|
||||
// return types is only used for validation. The claims variable that's available
|
||||
// to the claim mapping expressions is a map[string]interface{}, so we can't
|
||||
// really know what the return type is during compilation. Strict type checking
|
||||
// is done during evaluation.
|
||||
return []*celgo.Type{celgo.AnyType}
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &ClaimValidationCondition{}
|
||||
|
||||
// ClaimValidationCondition is a CEL expression that validates a claim.
|
||||
type ClaimValidationCondition struct {
|
||||
Expression string
|
||||
Message string
|
||||
}
|
||||
|
||||
// GetExpression returns the CEL expression.
|
||||
func (v *ClaimValidationCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// ReturnTypes returns the CEL expression return types.
|
||||
func (v *ClaimValidationCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.BoolType}
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &ExtraMappingExpression{}
|
||||
|
||||
// ExtraMappingExpression is a CEL expression that maps an extra to a list of values.
|
||||
type ExtraMappingExpression struct {
|
||||
Key string
|
||||
Expression string
|
||||
}
|
||||
|
||||
// GetExpression returns the CEL expression.
|
||||
func (v *ExtraMappingExpression) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// ReturnTypes returns the CEL expression return types.
|
||||
func (v *ExtraMappingExpression) ReturnTypes() []*celgo.Type {
|
||||
// return types is only used for validation. The claims variable that's available
|
||||
// to the claim mapping expressions is a map[string]interface{}, so we can't
|
||||
// really know what the return type is during compilation. Strict type checking
|
||||
// is done during evaluation.
|
||||
return []*celgo.Type{celgo.AnyType}
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &UserValidationCondition{}
|
||||
|
||||
// UserValidationCondition is a CEL expression that validates a User.
|
||||
type UserValidationCondition struct {
|
||||
Expression string
|
||||
Message string
|
||||
}
|
||||
|
||||
// GetExpression returns the CEL expression.
|
||||
func (v *UserValidationCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
// ReturnTypes returns the CEL expression return types.
|
||||
func (v *UserValidationCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.BoolType}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
var _ ClaimsMapper = &mapper{}
|
||||
var _ UserMapper = &mapper{}
|
||||
|
||||
// mapper implements the ClaimsMapper and UserMapper interface.
|
||||
type mapper struct {
|
||||
compilationResults []CompilationResult
|
||||
}
|
||||
|
||||
// CELMapper is a struct that holds the compiled expressions for
|
||||
// username, groups, uid, extra, claimValidation and userValidation
|
||||
type CELMapper struct {
|
||||
Username ClaimsMapper
|
||||
Groups ClaimsMapper
|
||||
UID ClaimsMapper
|
||||
Extra ClaimsMapper
|
||||
ClaimValidationRules ClaimsMapper
|
||||
UserValidationRules UserMapper
|
||||
}
|
||||
|
||||
// NewClaimsMapper returns a new ClaimsMapper.
|
||||
func NewClaimsMapper(compilationResults []CompilationResult) ClaimsMapper {
|
||||
return &mapper{
|
||||
compilationResults: compilationResults,
|
||||
}
|
||||
}
|
||||
|
||||
// NewUserMapper returns a new UserMapper.
|
||||
func NewUserMapper(compilationResults []CompilationResult) UserMapper {
|
||||
return &mapper{
|
||||
compilationResults: compilationResults,
|
||||
}
|
||||
}
|
||||
|
||||
// EvalClaimMapping evaluates the given claim mapping expression and returns a EvaluationResult.
|
||||
func (m *mapper) EvalClaimMapping(ctx context.Context, claims *unstructured.Unstructured) (EvaluationResult, error) {
|
||||
results, err := m.eval(ctx, map[string]interface{}{claimsVarName: claims.Object})
|
||||
if err != nil {
|
||||
return EvaluationResult{}, err
|
||||
}
|
||||
if len(results) != 1 {
|
||||
return EvaluationResult{}, fmt.Errorf("expected 1 evaluation result, got %d", len(results))
|
||||
}
|
||||
return results[0], nil
|
||||
}
|
||||
|
||||
// EvalClaimMappings evaluates the given expressions and returns a list of EvaluationResult.
|
||||
func (m *mapper) EvalClaimMappings(ctx context.Context, claims *unstructured.Unstructured) ([]EvaluationResult, error) {
|
||||
return m.eval(ctx, map[string]interface{}{claimsVarName: claims.Object})
|
||||
}
|
||||
|
||||
// EvalUser evaluates the given user expressions and returns a list of EvaluationResult.
|
||||
func (m *mapper) EvalUser(ctx context.Context, userInfo *unstructured.Unstructured) ([]EvaluationResult, error) {
|
||||
return m.eval(ctx, map[string]interface{}{userVarName: userInfo.Object})
|
||||
}
|
||||
|
||||
func (m *mapper) eval(ctx context.Context, input map[string]interface{}) ([]EvaluationResult, error) {
|
||||
evaluations := make([]EvaluationResult, len(m.compilationResults))
|
||||
|
||||
for i, compilationResult := range m.compilationResults {
|
||||
var evaluation = &evaluations[i]
|
||||
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
|
||||
|
||||
evalResult, _, err := compilationResult.Program.ContextEval(ctx, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("expression '%s' resulted in error: %w", compilationResult.ExpressionAccessor.GetExpression(), err)
|
||||
}
|
||||
|
||||
evaluation.EvalResult = evalResult
|
||||
}
|
||||
|
||||
return evaluations, nil
|
||||
}
|
@ -35,18 +35,25 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
"k8s.io/klog/v2"
|
||||
@ -165,6 +172,13 @@ type Authenticator struct {
|
||||
|
||||
// resolver is used to resolve distributed claims.
|
||||
resolver *claimResolver
|
||||
|
||||
// celMapper contains the compiled CEL expressions for
|
||||
// username, groups, uid, extra, claimMapping and claimValidation
|
||||
celMapper authenticationcel.CELMapper
|
||||
|
||||
// requiredClaims contains the list of claims that must be present in the token.
|
||||
requiredClaims map[string]string
|
||||
}
|
||||
|
||||
func (a *Authenticator) setVerifier(v *oidc.IDTokenVerifier) {
|
||||
@ -197,7 +211,8 @@ var allowedSigningAlgs = map[string]bool{
|
||||
}
|
||||
|
||||
func New(opts Options) (*Authenticator, error) {
|
||||
if err := apiservervalidation.ValidateJWTAuthenticator(opts.JWTAuthenticator).ToAggregate(); err != nil {
|
||||
celMapper, fieldErr := apiservervalidation.CompileAndValidateJWTAuthenticator(opts.JWTAuthenticator)
|
||||
if err := fieldErr.ToAggregate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -262,10 +277,19 @@ func New(opts Options) (*Authenticator, error) {
|
||||
resolver = newClaimResolver(groupsClaim, client, verifierConfig)
|
||||
}
|
||||
|
||||
requiredClaims := make(map[string]string)
|
||||
for _, claimValidationRule := range opts.JWTAuthenticator.ClaimValidationRules {
|
||||
if len(claimValidationRule.Claim) > 0 {
|
||||
requiredClaims[claimValidationRule.Claim] = claimValidationRule.RequiredValue
|
||||
}
|
||||
}
|
||||
|
||||
authenticator := &Authenticator{
|
||||
jwtAuthenticator: opts.JWTAuthenticator,
|
||||
cancel: cancel,
|
||||
resolver: resolver,
|
||||
celMapper: celMapper,
|
||||
requiredClaims: requiredClaims,
|
||||
}
|
||||
|
||||
if opts.KeySet != nil {
|
||||
@ -521,10 +545,130 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
|
||||
}
|
||||
}
|
||||
|
||||
var claimsUnstructured *unstructured.Unstructured
|
||||
// Convert the claims to unstructured so that we can evaluate the CEL expressions
|
||||
// against the claims. This is done once here so that we don't have to convert
|
||||
// the claims to unstructured multiple times in the CEL mapper for each mapping.
|
||||
// Only perform this conversion if any of the mapping or validation rules contain
|
||||
// CEL expressions.
|
||||
// TODO(aramase): In the future when we look into making distributed claims work,
|
||||
// we should see if we can skip this function and use a dynamic type resolver for
|
||||
// both json.RawMessage and the distributed claim fetching.
|
||||
if a.celMapper.Username != nil || a.celMapper.Groups != nil || a.celMapper.UID != nil || a.celMapper.Extra != nil || a.celMapper.ClaimValidationRules != nil {
|
||||
if claimsUnstructured, err = convertObjectToUnstructured(&c); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: could not convert claims to unstructured: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var username string
|
||||
if username, err = a.getUsername(ctx, c, claimsUnstructured); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
info := &user.DefaultInfo{Name: username}
|
||||
if info.Groups, err = a.getGroups(ctx, c, claimsUnstructured); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if info.UID, err = a.getUID(ctx, c, claimsUnstructured); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
extra, err := a.getExtra(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
info.Extra = extra
|
||||
}
|
||||
|
||||
// check to ensure all required claims are present in the ID token and have matching values.
|
||||
for claim, value := range a.requiredClaims {
|
||||
if !c.hasClaim(claim) {
|
||||
return nil, false, fmt.Errorf("oidc: required claim %s not present in ID token", claim)
|
||||
}
|
||||
|
||||
// NOTE: Only string values are supported as valid required claim values.
|
||||
var claimValue string
|
||||
if err := c.unmarshalClaim(claim, &claimValue); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse claim %s: %w", claim, err)
|
||||
}
|
||||
if claimValue != value {
|
||||
return nil, false, fmt.Errorf("oidc: required claim %s value does not match. Got = %s, want = %s", claim, claimValue, value)
|
||||
}
|
||||
}
|
||||
|
||||
if a.celMapper.ClaimValidationRules != nil {
|
||||
evalResult, err := a.celMapper.ClaimValidationRules.EvalClaimMappings(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: error evaluating claim validation expression: %w", err)
|
||||
}
|
||||
if err := checkValidationRulesEvaluation(evalResult, func(a authenticationcel.ExpressionAccessor) (string, error) {
|
||||
claimValidationCondition, ok := a.(*authenticationcel.ClaimValidationCondition)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid type conversion, expected ClaimValidationCondition")
|
||||
}
|
||||
return claimValidationCondition.Message, nil
|
||||
}); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: error evaluating claim validation expression: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if a.celMapper.UserValidationRules != nil {
|
||||
userInfo := &authenticationv1.UserInfo{
|
||||
Extra: make(map[string]authenticationv1.ExtraValue),
|
||||
Groups: info.GetGroups(),
|
||||
UID: info.GetUID(),
|
||||
Username: info.GetName(),
|
||||
}
|
||||
// Convert the extra information in the user object
|
||||
for key, val := range info.GetExtra() {
|
||||
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
|
||||
}
|
||||
|
||||
// Convert the user info to unstructured so that we can evaluate the CEL expressions
|
||||
// against the user info. This is done once here so that we don't have to convert
|
||||
// the user info to unstructured multiple times in the CEL mapper for each mapping.
|
||||
userInfoUnstructured, err := convertObjectToUnstructured(userInfo)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: could not convert user info to unstructured: %v", err)
|
||||
}
|
||||
|
||||
evalResult, err := a.celMapper.UserValidationRules.EvalUser(ctx, userInfoUnstructured)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: error evaluating user info validation rule: %w", err)
|
||||
}
|
||||
if err := checkValidationRulesEvaluation(evalResult, func(a authenticationcel.ExpressionAccessor) (string, error) {
|
||||
userValidationCondition, ok := a.(*authenticationcel.UserValidationCondition)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid type conversion, expected UserValidationCondition")
|
||||
}
|
||||
return userValidationCondition.Message, nil
|
||||
}); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: error evaluating user info validation rule: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &authenticator.Response{User: info}, true, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) getUsername(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) (string, error) {
|
||||
if a.celMapper.Username != nil {
|
||||
evalResult, err := a.celMapper.Username.EvalClaimMapping(ctx, claimsUnstructured)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", err)
|
||||
}
|
||||
if evalResult.EvalResult.Type() != celgo.StringType {
|
||||
return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", fmt.Errorf("username claim expression must return a string"))
|
||||
}
|
||||
|
||||
return evalResult.EvalResult.Value().(string), nil
|
||||
}
|
||||
|
||||
var username string
|
||||
usernameClaim := a.jwtAuthenticator.ClaimMappings.Username.Claim
|
||||
if err := c.unmarshalClaim(usernameClaim, &username); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse username claims %q: %v", usernameClaim, err)
|
||||
return "", fmt.Errorf("oidc: parse username claims %q: %v", usernameClaim, err)
|
||||
}
|
||||
|
||||
if usernameClaim == "email" {
|
||||
@ -533,24 +677,26 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
|
||||
if hasEmailVerified := c.hasClaim("email_verified"); hasEmailVerified {
|
||||
var emailVerified bool
|
||||
if err := c.unmarshalClaim("email_verified", &emailVerified); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse 'email_verified' claim: %v", err)
|
||||
return "", fmt.Errorf("oidc: parse 'email_verified' claim: %v", err)
|
||||
}
|
||||
|
||||
// If the email_verified claim is present we have to verify it is set to `true`.
|
||||
if !emailVerified {
|
||||
return nil, false, fmt.Errorf("oidc: email not verified")
|
||||
return "", fmt.Errorf("oidc: email not verified")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userNamePrefix := a.jwtAuthenticator.ClaimMappings.Username.Prefix
|
||||
if userNamePrefix != nil && *userNamePrefix != "" {
|
||||
username = *userNamePrefix + username
|
||||
return *userNamePrefix + username, nil
|
||||
}
|
||||
return username, nil
|
||||
}
|
||||
|
||||
info := &user.DefaultInfo{Name: username}
|
||||
func (a *Authenticator) getGroups(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) ([]string, error) {
|
||||
groupsClaim := a.jwtAuthenticator.ClaimMappings.Groups.Claim
|
||||
if groupsClaim != "" {
|
||||
if len(groupsClaim) > 0 {
|
||||
if _, ok := c[groupsClaim]; ok {
|
||||
// Some admins want to use string claims like "role" as the group value.
|
||||
// Allow the group claim to be a single string instead of an array.
|
||||
@ -558,39 +704,91 @@ func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*a
|
||||
// See: https://github.com/kubernetes/kubernetes/issues/33290
|
||||
var groups stringOrArray
|
||||
if err := c.unmarshalClaim(groupsClaim, &groups); err != nil {
|
||||
return nil, false, fmt.Errorf("oidc: parse groups claim %q: %v", groupsClaim, err)
|
||||
return nil, fmt.Errorf("oidc: parse groups claim %q: %w", groupsClaim, err)
|
||||
}
|
||||
info.Groups = []string(groups)
|
||||
|
||||
prefix := a.jwtAuthenticator.ClaimMappings.Groups.Prefix
|
||||
if prefix != nil && *prefix != "" {
|
||||
for i, group := range groups {
|
||||
groups[i] = *prefix + group
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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)
|
||||
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -1501,6 +1501,7 @@ k8s.io/apiserver/pkg/audit
|
||||
k8s.io/apiserver/pkg/audit/policy
|
||||
k8s.io/apiserver/pkg/authentication/authenticator
|
||||
k8s.io/apiserver/pkg/authentication/authenticatorfactory
|
||||
k8s.io/apiserver/pkg/authentication/cel
|
||||
k8s.io/apiserver/pkg/authentication/group
|
||||
k8s.io/apiserver/pkg/authentication/request/anonymous
|
||||
k8s.io/apiserver/pkg/authentication/request/bearertoken
|
||||
|
Loading…
Reference in New Issue
Block a user