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 }