Merge pull request #131536 from enj/enj/r/oidc_cel_activation

jwt: refactor CEL eval to drop `unstructured` and `map[string]any`

Kubernetes-commit: 1b517e55013479f674f3fac7196f3af307d6e23f
This commit is contained in:
Kubernetes Publisher 2025-04-29 18:47:55 -07:00
commit a18370ff46
3 changed files with 86 additions and 93 deletions

View File

@ -22,8 +22,7 @@ import (
celgo "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/google/cel-go/common/types/traits"
)
// ExpressionAccessor is an interface that provides access to a CEL expression.
@ -55,17 +54,17 @@ type Compiler interface {
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)
EvalClaimMapping(ctx context.Context, claims traits.Mapper) (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)
EvalClaimMappings(ctx context.Context, claims traits.Mapper) ([]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)
EvalUser(ctx context.Context, userInfo traits.Mapper) ([]EvaluationResult, error)
}
var _ ExpressionAccessor = &ClaimMappingExpression{}

View File

@ -20,7 +20,8 @@ import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/google/cel-go/common/types/traits"
"github.com/google/cel-go/interpreter"
)
var _ ClaimsMapper = &mapper{}
@ -57,8 +58,8 @@ func NewUserMapper(compilationResults []CompilationResult) UserMapper {
}
// 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})
func (m *mapper) EvalClaimMapping(ctx context.Context, claims traits.Mapper) (EvaluationResult, error) {
results, err := m.eval(ctx, &varNameActivation{name: claimsVarName, value: claims})
if err != nil {
return EvaluationResult{}, err
}
@ -69,16 +70,16 @@ func (m *mapper) EvalClaimMapping(ctx context.Context, claims *unstructured.Unst
}
// 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})
func (m *mapper) EvalClaimMappings(ctx context.Context, claims traits.Mapper) ([]EvaluationResult, error) {
return m.eval(ctx, &varNameActivation{name: claimsVarName, value: claims})
}
// 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) EvalUser(ctx context.Context, userInfo traits.Mapper) ([]EvaluationResult, error) {
return m.eval(ctx, &varNameActivation{name: userVarName, value: userInfo})
}
func (m *mapper) eval(ctx context.Context, input map[string]interface{}) ([]EvaluationResult, error) {
func (m *mapper) eval(ctx context.Context, input *varNameActivation) ([]EvaluationResult, error) {
evaluations := make([]EvaluationResult, len(m.compilationResults))
for i, compilationResult := range m.compilationResults {
@ -95,3 +96,19 @@ func (m *mapper) eval(ctx context.Context, input map[string]interface{}) ([]Eval
return evaluations, nil
}
var _ interpreter.Activation = &varNameActivation{}
type varNameActivation struct {
name string
value traits.Mapper
}
func (v *varNameActivation) ResolveName(name string) (any, bool) {
if v.name != name {
return nil, false
}
return v.value, true
}
func (v *varNameActivation) Parent() interpreter.Activation { return nil }

View File

@ -36,7 +36,6 @@ import (
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strings"
"sync"
"sync/atomic"
@ -44,11 +43,9 @@ import (
"github.com/coreos/go-oidc"
celgo "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"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/sets"
"k8s.io/apimachinery/pkg/util/wait"
@ -58,6 +55,7 @@ import (
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
authenticationtokenjwt "k8s.io/apiserver/pkg/authentication/token/jwt"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/cel/lazy"
certutil "k8s.io/client-go/util/cert"
"k8s.io/klog/v2"
)
@ -708,36 +706,31 @@ func (a *jwtAuthenticator) AuthenticateToken(ctx context.Context, token string)
}
}
var claimsUnstructured *unstructured.Unstructured
// Convert the claims to unstructured so that we can evaluate the CEL expressions
var claimsValue *lazy.MapValue
// Convert the claims to traits.Mapper 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.
// the claims to traits.Mapper 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.
// CEL expressions. The traits.Mapper is lazily evaluated against the expressions.
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)
}
claimsValue = newClaimsValue(c)
}
var username string
if username, err = a.getUsername(ctx, c, claimsUnstructured); err != nil {
if username, err = a.getUsername(ctx, c, claimsValue); err != nil {
return nil, false, err
}
info := &user.DefaultInfo{Name: username}
if info.Groups, err = a.getGroups(ctx, c, claimsUnstructured); err != nil {
if info.Groups, err = a.getGroups(ctx, c, claimsValue); err != nil {
return nil, false, err
}
if info.UID, err = a.getUID(ctx, c, claimsUnstructured); err != nil {
if info.UID, err = a.getUID(ctx, c, claimsValue); err != nil {
return nil, false, err
}
extra, err := a.getExtra(ctx, c, claimsUnstructured)
extra, err := a.getExtra(ctx, c, claimsValue)
if err != nil {
return nil, false, err
}
@ -762,7 +755,7 @@ func (a *jwtAuthenticator) AuthenticateToken(ctx context.Context, token string)
}
if a.celMapper.ClaimValidationRules != nil {
evalResult, err := a.celMapper.ClaimValidationRules.EvalClaimMappings(ctx, claimsUnstructured)
evalResult, err := a.celMapper.ClaimValidationRules.EvalClaimMappings(ctx, claimsValue)
if err != nil {
return nil, false, fmt.Errorf("oidc: error evaluating claim validation expression: %w", err)
}
@ -778,15 +771,13 @@ func (a *jwtAuthenticator) AuthenticateToken(ctx context.Context, token string)
}
if a.celMapper.UserValidationRules != nil {
// Convert the user info to unstructured so that we can evaluate the CEL expressions
// Convert the user info to traits.Mapper 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 := convertUserInfoToUnstructured(info)
if err != nil {
return nil, false, fmt.Errorf("oidc: could not convert user info to unstructured: %w", err)
}
// the user info to traits.Mapper multiple times in the CEL mapper for each mapping.
// The traits.Mapper is lazily evaluated against the expressions.
userInfoVal := newUserInfoValue(info)
evalResult, err := a.celMapper.UserValidationRules.EvalUser(ctx, userInfoUnstructured)
evalResult, err := a.celMapper.UserValidationRules.EvalUser(ctx, userInfoVal)
if err != nil {
return nil, false, fmt.Errorf("oidc: error evaluating user info validation rule: %w", err)
}
@ -812,9 +803,9 @@ func (a *jwtAuthenticator) HealthCheck() error {
return nil
}
func (a *jwtAuthenticator) getUsername(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) (string, error) {
func (a *jwtAuthenticator) getUsername(ctx context.Context, c claims, claimsValue *lazy.MapValue) (string, error) {
if a.celMapper.Username != nil {
evalResult, err := a.celMapper.Username.EvalClaimMapping(ctx, claimsUnstructured)
evalResult, err := a.celMapper.Username.EvalClaimMapping(ctx, claimsValue)
if err != nil {
return "", fmt.Errorf("oidc: error evaluating username claim expression: %w", err)
}
@ -860,7 +851,7 @@ func (a *jwtAuthenticator) getUsername(ctx context.Context, c claims, claimsUnst
return username, nil
}
func (a *jwtAuthenticator) getGroups(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) ([]string, error) {
func (a *jwtAuthenticator) getGroups(ctx context.Context, c claims, claimsValue *lazy.MapValue) ([]string, error) {
groupsClaim := a.jwtAuthenticator.ClaimMappings.Groups.Claim
if len(groupsClaim) > 0 {
if _, ok := c[groupsClaim]; ok {
@ -888,7 +879,7 @@ func (a *jwtAuthenticator) getGroups(ctx context.Context, c claims, claimsUnstru
return nil, nil
}
evalResult, err := a.celMapper.Groups.EvalClaimMapping(ctx, claimsUnstructured)
evalResult, err := a.celMapper.Groups.EvalClaimMapping(ctx, claimsValue)
if err != nil {
return nil, fmt.Errorf("oidc: error evaluating group claim expression: %w", err)
}
@ -900,7 +891,7 @@ func (a *jwtAuthenticator) getGroups(ctx context.Context, c claims, claimsUnstru
return groups, nil
}
func (a *jwtAuthenticator) getUID(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) (string, error) {
func (a *jwtAuthenticator) getUID(ctx context.Context, c claims, claimsValue *lazy.MapValue) (string, error) {
uidClaim := a.jwtAuthenticator.ClaimMappings.UID.Claim
if len(uidClaim) > 0 {
var uid string
@ -914,7 +905,7 @@ func (a *jwtAuthenticator) getUID(ctx context.Context, c claims, claimsUnstructu
return "", nil
}
evalResult, err := a.celMapper.UID.EvalClaimMapping(ctx, claimsUnstructured)
evalResult, err := a.celMapper.UID.EvalClaimMapping(ctx, claimsValue)
if err != nil {
return "", fmt.Errorf("oidc: error evaluating uid claim expression: %w", err)
}
@ -925,7 +916,7 @@ func (a *jwtAuthenticator) getUID(ctx context.Context, c claims, claimsUnstructu
return evalResult.EvalResult.Value().(string), nil
}
func (a *jwtAuthenticator) getExtra(ctx context.Context, c claims, claimsUnstructured *unstructured.Unstructured) (map[string][]string, error) {
func (a *jwtAuthenticator) getExtra(ctx context.Context, c claims, claimsValue *lazy.MapValue) (map[string][]string, error) {
extra := make(map[string][]string)
if credentialID := getCredentialID(c); len(credentialID) > 0 {
@ -936,7 +927,7 @@ func (a *jwtAuthenticator) getExtra(ctx context.Context, c claims, claimsUnstruc
return extra, nil
}
evalResult, err := a.celMapper.Extra.EvalClaimMappings(ctx, claimsUnstructured)
evalResult, err := a.celMapper.Extra.EvalClaimMappings(ctx, claimsValue)
if err != nil {
return nil, err
}
@ -1023,7 +1014,7 @@ func (c claims) unmarshalClaim(name string, v interface{}) error {
if !ok {
return fmt.Errorf("claim not present")
}
return json.Unmarshal([]byte(val), v)
return json.Unmarshal(val, v)
}
func (c claims) hasClaim(name string) bool {
@ -1033,6 +1024,25 @@ func (c claims) hasClaim(name string) bool {
return true
}
func newClaimsValue(c claims) *lazy.MapValue {
lazyMap := lazy.NewMapValue(types.NewObjectType("kubernetes.claims"))
for name, msg := range c { // TODO add distributed claims support
lazyMap.Append(name, func(_ *lazy.MapValue) ref.Val {
data, err := msg.MarshalJSON()
if err != nil {
return types.WrapErr(err) // impossible since RawMessage never errors
}
var value any // TODO how do we do multiple levels of lazy decoding?
if err := json.Unmarshal(data, &value); err != nil {
return types.NewErr("claim %q failed to unmarshal: %w", name, err)
}
return types.DefaultTypeAdapter.NativeToValue(value)
})
}
return lazyMap
}
// 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.
@ -1113,50 +1123,17 @@ func checkValidationRulesEvaluation(results []authenticationcel.EvaluationResult
return nil
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
func newUserInfoValue(info user.Info) *lazy.MapValue {
lazyMap := lazy.NewMapValue(types.NewObjectType("kubernetes.UserInfo"))
field := func(name string, get func() any) {
lazyMap.Append(name, func(_ *lazy.MapValue) ref.Val {
value := get()
return types.DefaultTypeAdapter.NativeToValue(value)
})
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func convertUserInfoToUnstructured(info user.Info) (*unstructured.Unstructured, error) {
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, err
}
// check if the user info contains the required fields. If not, set them to empty values.
// This is done because the CEL expressions expect these fields to be present.
if userInfoUnstructured.Object["username"] == nil {
userInfoUnstructured.Object["username"] = ""
}
if userInfoUnstructured.Object["uid"] == nil {
userInfoUnstructured.Object["uid"] = ""
}
if userInfoUnstructured.Object["groups"] == nil {
userInfoUnstructured.Object["groups"] = []string{}
}
if userInfoUnstructured.Object["extra"] == nil {
userInfoUnstructured.Object["extra"] = map[string]authenticationv1.ExtraValue{}
}
return userInfoUnstructured, nil
field("username", func() any { return info.GetName() })
field("uid", func() any { return info.GetUID() })
field("groups", func() any { return info.GetGroups() })
field("extra", func() any { return info.GetExtra() })
return lazyMap
}