diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go index 0fc208414e4..be7e634f367 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/interface.go @@ -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{} diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go b/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go index ab308bb7f0f..3b6041aeb5b 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/cel/mapper.go @@ -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 } diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go index d5fd28f0ebe..dfe1e98df0a 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go @@ -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 }