k8s.io/apiserver@v0.31.1/pkg/admission/plugin/cel/filter.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cel
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"math"
    23  	"reflect"
    24  	"time"
    25  
    26  	"github.com/google/cel-go/interpreter"
    27  
    28  	admissionv1 "k8s.io/api/admission/v1"
    29  	authenticationv1 "k8s.io/api/authentication/v1"
    30  	v1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/apiserver/pkg/admission"
    35  	"k8s.io/apiserver/pkg/cel"
    36  	"k8s.io/apiserver/pkg/cel/environment"
    37  	"k8s.io/apiserver/pkg/cel/library"
    38  )
    39  
    40  // filterCompiler implement the interface FilterCompiler.
    41  type filterCompiler struct {
    42  	compiler Compiler
    43  }
    44  
    45  func NewFilterCompiler(env *environment.EnvSet) FilterCompiler {
    46  	return &filterCompiler{compiler: NewCompiler(env)}
    47  }
    48  
    49  type evaluationActivation struct {
    50  	object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
    51  }
    52  
    53  // ResolveName returns a value from the activation by qualified name, or false if the name
    54  // could not be found.
    55  func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
    56  	switch name {
    57  	case ObjectVarName:
    58  		return a.object, true
    59  	case OldObjectVarName:
    60  		return a.oldObject, true
    61  	case ParamsVarName:
    62  		return a.params, true // params may be null
    63  	case RequestVarName:
    64  		return a.request, true
    65  	case NamespaceVarName:
    66  		return a.namespace, true
    67  	case AuthorizerVarName:
    68  		return a.authorizer, a.authorizer != nil
    69  	case RequestResourceAuthorizerVarName:
    70  		return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
    71  	case VariableVarName: // variables always present
    72  		return a.variables, true
    73  	default:
    74  		return nil, false
    75  	}
    76  }
    77  
    78  // Parent returns the parent of the current activation, may be nil.
    79  // If non-nil, the parent will be searched during resolve calls.
    80  func (a *evaluationActivation) Parent() interpreter.Activation {
    81  	return nil
    82  }
    83  
    84  // Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
    85  func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) Filter {
    86  	compilationResults := make([]CompilationResult, len(expressionAccessors))
    87  	for i, expressionAccessor := range expressionAccessors {
    88  		if expressionAccessor == nil {
    89  			continue
    90  		}
    91  		compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
    92  	}
    93  	return NewFilter(compilationResults)
    94  }
    95  
    96  // filter implements the Filter interface
    97  type filter struct {
    98  	compilationResults []CompilationResult
    99  }
   100  
   101  func NewFilter(compilationResults []CompilationResult) Filter {
   102  	return &filter{
   103  		compilationResults,
   104  	}
   105  }
   106  
   107  func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
   108  	if obj == nil || reflect.ValueOf(obj).IsNil() {
   109  		return &unstructured.Unstructured{Object: nil}, nil
   110  	}
   111  	ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	return &unstructured.Unstructured{Object: ret}, nil
   116  }
   117  
   118  func objectToResolveVal(r runtime.Object) (interface{}, error) {
   119  	if r == nil || reflect.ValueOf(r).IsNil() {
   120  		return nil, nil
   121  	}
   122  	v, err := convertObjectToUnstructured(r)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  	return v.Object, nil
   127  }
   128  
   129  // ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
   130  // errors per evaluation are returned on the Evaluation object
   131  // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
   132  func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
   133  	// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
   134  	evaluations := make([]EvaluationResult, len(f.compilationResults))
   135  	var err error
   136  
   137  	oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
   138  	if err != nil {
   139  		return nil, -1, err
   140  	}
   141  	objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
   142  	if err != nil {
   143  		return nil, -1, err
   144  	}
   145  	var paramsVal, authorizerVal, requestResourceAuthorizerVal any
   146  	if inputs.VersionedParams != nil {
   147  		paramsVal, err = objectToResolveVal(inputs.VersionedParams)
   148  		if err != nil {
   149  			return nil, -1, err
   150  		}
   151  	}
   152  
   153  	if inputs.Authorizer != nil {
   154  		authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
   155  		requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
   156  	}
   157  
   158  	requestVal, err := convertObjectToUnstructured(request)
   159  	if err != nil {
   160  		return nil, -1, err
   161  	}
   162  	namespaceVal, err := objectToResolveVal(namespace)
   163  	if err != nil {
   164  		return nil, -1, err
   165  	}
   166  	va := &evaluationActivation{
   167  		object:                    objectVal,
   168  		oldObject:                 oldObjectVal,
   169  		params:                    paramsVal,
   170  		request:                   requestVal.Object,
   171  		namespace:                 namespaceVal,
   172  		authorizer:                authorizerVal,
   173  		requestResourceAuthorizer: requestResourceAuthorizerVal,
   174  	}
   175  
   176  	// composition is an optional feature that only applies for ValidatingAdmissionPolicy.
   177  	// check if the context allows composition
   178  	var compositionCtx CompositionContext
   179  	var ok bool
   180  	if compositionCtx, ok = ctx.(CompositionContext); ok {
   181  		va.variables = compositionCtx.Variables(va)
   182  	}
   183  
   184  	remainingBudget := runtimeCELCostBudget
   185  	for i, compilationResult := range f.compilationResults {
   186  		var evaluation = &evaluations[i]
   187  		if compilationResult.ExpressionAccessor == nil { // in case of placeholder
   188  			continue
   189  		}
   190  		evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
   191  		if compilationResult.Error != nil {
   192  			evaluation.Error = &cel.Error{
   193  				Type:   cel.ErrorTypeInvalid,
   194  				Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
   195  				Cause:  compilationResult.Error,
   196  			}
   197  			continue
   198  		}
   199  		if compilationResult.Program == nil {
   200  			evaluation.Error = &cel.Error{
   201  				Type:   cel.ErrorTypeInternal,
   202  				Detail: fmt.Sprintf("unexpected internal error compiling expression"),
   203  			}
   204  			continue
   205  		}
   206  		t1 := time.Now()
   207  		evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, va)
   208  		// budget may be spent due to lazy evaluation of composited variables
   209  		if compositionCtx != nil {
   210  			compositionCost := compositionCtx.GetAndResetCost()
   211  			if compositionCost > remainingBudget {
   212  				return nil, -1, &cel.Error{
   213  					Type:   cel.ErrorTypeInvalid,
   214  					Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
   215  					Cause:  cel.ErrOutOfBudget,
   216  				}
   217  			}
   218  			remainingBudget -= compositionCost
   219  		}
   220  		elapsed := time.Since(t1)
   221  		evaluation.Elapsed = elapsed
   222  		if evalDetails == nil {
   223  			return nil, -1, &cel.Error{
   224  				Type:   cel.ErrorTypeInternal,
   225  				Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
   226  			}
   227  		} else {
   228  			rtCost := evalDetails.ActualCost()
   229  			if rtCost == nil {
   230  				return nil, -1, &cel.Error{
   231  					Type:   cel.ErrorTypeInvalid,
   232  					Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
   233  					Cause:  cel.ErrOutOfBudget,
   234  				}
   235  			} else {
   236  				if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
   237  					return nil, -1, &cel.Error{
   238  						Type:   cel.ErrorTypeInvalid,
   239  						Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
   240  						Cause:  cel.ErrOutOfBudget,
   241  					}
   242  				}
   243  				remainingBudget -= int64(*rtCost)
   244  			}
   245  		}
   246  		if err != nil {
   247  			evaluation.Error = &cel.Error{
   248  				Type:   cel.ErrorTypeInvalid,
   249  				Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
   250  			}
   251  		} else {
   252  			evaluation.EvalResult = evalResult
   253  		}
   254  	}
   255  
   256  	return evaluations, remainingBudget, nil
   257  }
   258  
   259  // TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
   260  func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
   261  	// Attempting to use same logic as webhook for constructing resource
   262  	// GVK, GVR, subresource
   263  	// Use the GVK, GVR that the matcher decided was equivalent to that of the request
   264  	// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
   265  	gvk := equivalentKind
   266  	gvr := equivalentGVR
   267  	subresource := attr.GetSubresource()
   268  
   269  	requestGVK := attr.GetKind()
   270  	requestGVR := attr.GetResource()
   271  	requestSubResource := attr.GetSubresource()
   272  
   273  	aUserInfo := attr.GetUserInfo()
   274  	var userInfo authenticationv1.UserInfo
   275  	if aUserInfo != nil {
   276  		userInfo = authenticationv1.UserInfo{
   277  			Extra:    make(map[string]authenticationv1.ExtraValue),
   278  			Groups:   aUserInfo.GetGroups(),
   279  			UID:      aUserInfo.GetUID(),
   280  			Username: aUserInfo.GetName(),
   281  		}
   282  		// Convert the extra information in the user object
   283  		for key, val := range aUserInfo.GetExtra() {
   284  			userInfo.Extra[key] = authenticationv1.ExtraValue(val)
   285  		}
   286  	}
   287  
   288  	dryRun := attr.IsDryRun()
   289  
   290  	return &admissionv1.AdmissionRequest{
   291  		Kind: metav1.GroupVersionKind{
   292  			Group:   gvk.Group,
   293  			Kind:    gvk.Kind,
   294  			Version: gvk.Version,
   295  		},
   296  		Resource: metav1.GroupVersionResource{
   297  			Group:    gvr.Group,
   298  			Resource: gvr.Resource,
   299  			Version:  gvr.Version,
   300  		},
   301  		SubResource: subresource,
   302  		RequestKind: &metav1.GroupVersionKind{
   303  			Group:   requestGVK.Group,
   304  			Kind:    requestGVK.Kind,
   305  			Version: requestGVK.Version,
   306  		},
   307  		RequestResource: &metav1.GroupVersionResource{
   308  			Group:    requestGVR.Group,
   309  			Resource: requestGVR.Resource,
   310  			Version:  requestGVR.Version,
   311  		},
   312  		RequestSubResource: requestSubResource,
   313  		Name:               attr.GetName(),
   314  		Namespace:          attr.GetNamespace(),
   315  		Operation:          admissionv1.Operation(attr.GetOperation()),
   316  		UserInfo:           userInfo,
   317  		// Leave Object and OldObject unset since we don't provide access to them via request
   318  		DryRun: &dryRun,
   319  		Options: runtime.RawExtension{
   320  			Object: attr.GetOperationOptions(),
   321  		},
   322  	}
   323  }
   324  
   325  // CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
   326  // If the namespace is nil, CreateNamespaceObject returns nil
   327  func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
   328  	if namespace == nil {
   329  		return nil
   330  	}
   331  
   332  	return &v1.Namespace{
   333  		Status: namespace.Status,
   334  		Spec:   namespace.Spec,
   335  		ObjectMeta: metav1.ObjectMeta{
   336  			Name:                       namespace.Name,
   337  			GenerateName:               namespace.GenerateName,
   338  			Namespace:                  namespace.Namespace,
   339  			UID:                        namespace.UID,
   340  			ResourceVersion:            namespace.ResourceVersion,
   341  			Generation:                 namespace.Generation,
   342  			CreationTimestamp:          namespace.CreationTimestamp,
   343  			DeletionTimestamp:          namespace.DeletionTimestamp,
   344  			DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
   345  			Labels:                     namespace.Labels,
   346  			Annotations:                namespace.Annotations,
   347  			Finalizers:                 namespace.Finalizers,
   348  		},
   349  	}
   350  }
   351  
   352  // CompilationErrors returns a list of all the errors from the compilation of the evaluator
   353  func (e *filter) CompilationErrors() []error {
   354  	compilationErrors := []error{}
   355  	for _, result := range e.compilationResults {
   356  		if result.Error != nil {
   357  			compilationErrors = append(compilationErrors, result.Error)
   358  		}
   359  	}
   360  	return compilationErrors
   361  }