k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/validating/dispatcher.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 validating
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  
    25  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    26  	v1 "k8s.io/api/core/v1"
    27  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	utiljson "k8s.io/apimachinery/pkg/util/json"
    32  	"k8s.io/apiserver/pkg/admission"
    33  	"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
    34  	celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
    35  	celconfig "k8s.io/apiserver/pkg/apis/cel"
    36  	"k8s.io/apiserver/pkg/authorization/authorizer"
    37  	"k8s.io/apiserver/pkg/warning"
    38  	"k8s.io/klog/v2"
    39  )
    40  
    41  type dispatcher struct {
    42  	matcher generic.PolicyMatcher
    43  	authz   authorizer.Authorizer
    44  }
    45  
    46  var _ generic.Dispatcher[PolicyHook] = &dispatcher{}
    47  
    48  func NewDispatcher(
    49  	authorizer authorizer.Authorizer,
    50  	matcher generic.PolicyMatcher,
    51  ) generic.Dispatcher[PolicyHook] {
    52  	return &dispatcher{
    53  		matcher: matcher,
    54  		authz:   authorizer,
    55  	}
    56  }
    57  
    58  // contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
    59  // that determined the decision
    60  type policyDecisionWithMetadata struct {
    61  	PolicyDecision
    62  	Definition *admissionregistrationv1.ValidatingAdmissionPolicy
    63  	Binding    *admissionregistrationv1.ValidatingAdmissionPolicyBinding
    64  }
    65  
    66  // Dispatch implements generic.Dispatcher.
    67  func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
    68  
    69  	var deniedDecisions []policyDecisionWithMetadata
    70  
    71  	addConfigError := func(err error, definition *admissionregistrationv1.ValidatingAdmissionPolicy, binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding) {
    72  		// we always default the FailurePolicy if it is unset and validate it in API level
    73  		var policy admissionregistrationv1.FailurePolicyType
    74  		if definition.Spec.FailurePolicy == nil {
    75  			policy = admissionregistrationv1.Fail
    76  		} else {
    77  			policy = *definition.Spec.FailurePolicy
    78  		}
    79  
    80  		// apply FailurePolicy specified in ValidatingAdmissionPolicy, the default would be Fail
    81  		switch policy {
    82  		case admissionregistrationv1.Ignore:
    83  			// TODO: add metrics for ignored error here
    84  			return
    85  		case admissionregistrationv1.Fail:
    86  			var message string
    87  			if binding == nil {
    88  				message = fmt.Errorf("failed to configure policy: %w", err).Error()
    89  			} else {
    90  				message = fmt.Errorf("failed to configure binding: %w", err).Error()
    91  			}
    92  			deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
    93  				PolicyDecision: PolicyDecision{
    94  					Action:  ActionDeny,
    95  					Message: message,
    96  				},
    97  				Definition: definition,
    98  				Binding:    binding,
    99  			})
   100  		default:
   101  			deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
   102  				PolicyDecision: PolicyDecision{
   103  					Action:  ActionDeny,
   104  					Message: fmt.Errorf("unrecognized failure policy: '%v'", policy).Error(),
   105  				},
   106  				Definition: definition,
   107  				Binding:    binding,
   108  			})
   109  		}
   110  	}
   111  
   112  	authz := newCachingAuthorizer(c.authz)
   113  
   114  	for _, hook := range hooks {
   115  		// versionedAttributes will be set to non-nil inside of the loop, but
   116  		// is scoped outside of the param loop so we only convert once. We defer
   117  		// conversion so that it is only performed when we know a policy matches,
   118  		// saving the cost of converting non-matching requests.
   119  		var versionedAttr *admission.VersionedAttributes
   120  
   121  		definition := hook.Policy
   122  		matches, matchResource, matchKind, err := c.matcher.DefinitionMatches(a, o, NewValidatingAdmissionPolicyAccessor(definition))
   123  		if err != nil {
   124  			// Configuration error.
   125  			addConfigError(err, definition, nil)
   126  			continue
   127  		}
   128  		if !matches {
   129  			// Policy definition does not match request
   130  			continue
   131  		} else if hook.ConfigurationError != nil {
   132  			// Configuration error.
   133  			addConfigError(hook.ConfigurationError, definition, nil)
   134  			continue
   135  		}
   136  
   137  		auditAnnotationCollector := newAuditAnnotationCollector()
   138  		for _, binding := range hook.Bindings {
   139  			// If the key is inside dependentBindings, there is guaranteed to
   140  			// be a bindingInfo for it
   141  			matches, err := c.matcher.BindingMatches(a, o, NewValidatingAdmissionPolicyBindingAccessor(binding))
   142  			if err != nil {
   143  				// Configuration error.
   144  				addConfigError(err, definition, binding)
   145  				continue
   146  			}
   147  			if !matches {
   148  				continue
   149  			}
   150  
   151  			params, err := generic.CollectParams(
   152  				hook.Policy.Spec.ParamKind,
   153  				hook.ParamInformer,
   154  				hook.ParamScope,
   155  				binding.Spec.ParamRef,
   156  				a.GetNamespace(),
   157  			)
   158  
   159  			if err != nil {
   160  				addConfigError(err, definition, binding)
   161  				continue
   162  			} else if versionedAttr == nil && len(params) > 0 {
   163  				// As optimization versionedAttr creation is deferred until
   164  				// first use. Since > 0 params, we will validate
   165  				va, err := admission.NewVersionedAttributes(a, matchKind, o)
   166  				if err != nil {
   167  					wrappedErr := fmt.Errorf("failed to convert object version: %w", err)
   168  					addConfigError(wrappedErr, definition, binding)
   169  					continue
   170  				}
   171  				versionedAttr = va
   172  			}
   173  
   174  			var validationResults []ValidateResult
   175  			var namespace *v1.Namespace
   176  			namespaceName := a.GetNamespace()
   177  
   178  			// Special case, the namespace object has the namespace of itself (maybe a bug).
   179  			// unset it if the incoming object is a namespace
   180  			if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
   181  				namespaceName = ""
   182  			}
   183  
   184  			// if it is cluster scoped, namespaceName will be empty
   185  			// Otherwise, get the Namespace resource.
   186  			if namespaceName != "" {
   187  				namespace, err = c.matcher.GetNamespace(namespaceName)
   188  				if err != nil {
   189  					return err
   190  				}
   191  			}
   192  
   193  			for _, param := range params {
   194  				var p runtime.Object = param
   195  				if p != nil && p.GetObjectKind().GroupVersionKind().Empty() {
   196  					// Make sure param has TypeMeta populated
   197  					// This is a simple hack to make sure typeMeta is
   198  					// available to CEL without making copies of objects, etc.
   199  					p = &wrappedParam{
   200  						TypeMeta: metav1.TypeMeta{
   201  							APIVersion: definition.Spec.ParamKind.APIVersion,
   202  							Kind:       definition.Spec.ParamKind.Kind,
   203  						},
   204  						nested: param,
   205  					}
   206  				}
   207  
   208  				validationResults = append(validationResults,
   209  					hook.Evaluator.Validate(
   210  						ctx,
   211  						matchResource,
   212  						versionedAttr,
   213  						p,
   214  						namespace,
   215  						celconfig.RuntimeCELCostBudget,
   216  						authz,
   217  					),
   218  				)
   219  			}
   220  
   221  			for _, validationResult := range validationResults {
   222  				for i, decision := range validationResult.Decisions {
   223  					switch decision.Action {
   224  					case ActionAdmit:
   225  						if decision.Evaluation == EvalError {
   226  							celmetrics.Metrics.ObserveAdmission(ctx, decision.Elapsed, definition.Name, binding.Name, ErrorType(&decision))
   227  						}
   228  					case ActionDeny:
   229  						for _, action := range binding.Spec.ValidationActions {
   230  							switch action {
   231  							case admissionregistrationv1.Deny:
   232  								deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
   233  									Definition:     definition,
   234  									Binding:        binding,
   235  									PolicyDecision: decision,
   236  								})
   237  								celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, ErrorType(&decision))
   238  							case admissionregistrationv1.Audit:
   239  								publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
   240  								celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, ErrorType(&decision))
   241  							case admissionregistrationv1.Warn:
   242  								warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
   243  								celmetrics.Metrics.ObserveWarn(ctx, decision.Elapsed, definition.Name, binding.Name, ErrorType(&decision))
   244  							}
   245  						}
   246  					default:
   247  						return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
   248  							decision.Action, binding.Name, definition.Name)
   249  					}
   250  				}
   251  
   252  				for _, auditAnnotation := range validationResult.AuditAnnotations {
   253  					switch auditAnnotation.Action {
   254  					case AuditAnnotationActionPublish:
   255  						value := auditAnnotation.Value
   256  						if len(auditAnnotation.Value) > maxAuditAnnotationValueLength {
   257  							value = value[:maxAuditAnnotationValueLength]
   258  						}
   259  						auditAnnotationCollector.add(auditAnnotation.Key, value)
   260  					case AuditAnnotationActionError:
   261  						// When failurePolicy=fail, audit annotation errors result in deny
   262  						d := policyDecisionWithMetadata{
   263  							Definition: definition,
   264  							Binding:    binding,
   265  							PolicyDecision: PolicyDecision{
   266  								Action:     ActionDeny,
   267  								Evaluation: EvalError,
   268  								Message:    auditAnnotation.Error,
   269  								Elapsed:    auditAnnotation.Elapsed,
   270  							},
   271  						}
   272  						deniedDecisions = append(deniedDecisions, d)
   273  						celmetrics.Metrics.ObserveRejection(ctx, auditAnnotation.Elapsed, definition.Name, binding.Name, ErrorType(&d.PolicyDecision))
   274  					case AuditAnnotationActionExclude: // skip it
   275  					default:
   276  						return fmt.Errorf("unsupported AuditAnnotation Action: %s", auditAnnotation.Action)
   277  					}
   278  				}
   279  			}
   280  		}
   281  		auditAnnotationCollector.publish(definition.Name, a)
   282  	}
   283  
   284  	if len(deniedDecisions) > 0 {
   285  		// TODO: refactor admission.NewForbidden so the name extraction is reusable but the code/reason is customizable
   286  		var message string
   287  		deniedDecision := deniedDecisions[0]
   288  		if deniedDecision.Binding != nil {
   289  			message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' with binding '%s' denied request: %s", deniedDecision.Definition.Name, deniedDecision.Binding.Name, deniedDecision.Message)
   290  		} else {
   291  			message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' denied request: %s", deniedDecision.Definition.Name, deniedDecision.Message)
   292  		}
   293  		err := admission.NewForbidden(a, errors.New(message)).(*k8serrors.StatusError)
   294  		reason := deniedDecision.Reason
   295  		if len(reason) == 0 {
   296  			reason = metav1.StatusReasonInvalid
   297  		}
   298  		err.ErrStatus.Reason = reason
   299  		err.ErrStatus.Code = reasonToCode(reason)
   300  		err.ErrStatus.Details.Causes = append(err.ErrStatus.Details.Causes, metav1.StatusCause{Message: message})
   301  		return err
   302  	}
   303  	return nil
   304  }
   305  
   306  func publishValidationFailureAnnotation(binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
   307  	key := "validation.policy.admission.k8s.io/validation_failure"
   308  	// Marshal to a list of failures since, in the future, we may need to support multiple failures
   309  	valueJSON, err := utiljson.Marshal([]ValidationFailureValue{{
   310  		ExpressionIndex:   expressionIndex,
   311  		Message:           decision.Message,
   312  		ValidationActions: binding.Spec.ValidationActions,
   313  		Binding:           binding.Name,
   314  		Policy:            binding.Spec.PolicyName,
   315  	}})
   316  	if err != nil {
   317  		klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err)
   318  	}
   319  	value := string(valueJSON)
   320  	if err := attributes.AddAnnotation(key, value); err != nil {
   321  		klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err)
   322  	}
   323  }
   324  
   325  const maxAuditAnnotationValueLength = 10 * 1024
   326  
   327  // validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
   328  // annotation value.
   329  type ValidationFailureValue struct {
   330  	Message           string                                     `json:"message"`
   331  	Policy            string                                     `json:"policy"`
   332  	Binding           string                                     `json:"binding"`
   333  	ExpressionIndex   int                                        `json:"expressionIndex"`
   334  	ValidationActions []admissionregistrationv1.ValidationAction `json:"validationActions"`
   335  }
   336  
   337  type auditAnnotationCollector struct {
   338  	annotations map[string][]string
   339  }
   340  
   341  func newAuditAnnotationCollector() auditAnnotationCollector {
   342  	return auditAnnotationCollector{annotations: map[string][]string{}}
   343  }
   344  
   345  func (a auditAnnotationCollector) add(key, value string) {
   346  	// If multiple bindings produces the exact same key and value for an audit annotation,
   347  	// ignore the duplicates.
   348  	for _, v := range a.annotations[key] {
   349  		if v == value {
   350  			return
   351  		}
   352  	}
   353  	a.annotations[key] = append(a.annotations[key], value)
   354  }
   355  
   356  func (a auditAnnotationCollector) publish(policyName string, attributes admission.Attributes) {
   357  	for key, bindingAnnotations := range a.annotations {
   358  		var value string
   359  		if len(bindingAnnotations) == 1 {
   360  			value = bindingAnnotations[0]
   361  		} else {
   362  			// Multiple distinct values can exist when binding params are used in the valueExpression of an auditAnnotation.
   363  			// When this happens, the values are concatenated into a comma-separated list.
   364  			value = strings.Join(bindingAnnotations, ", ")
   365  		}
   366  		if err := attributes.AddAnnotation(policyName+"/"+key, value); err != nil {
   367  			klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s: %v", key, value, policyName, err)
   368  		}
   369  	}
   370  }
   371  
   372  // A workaround to fact that native types do not have TypeMeta populated, which
   373  // is needed for CEL expressions to be able to access the value.
   374  type wrappedParam struct {
   375  	metav1.TypeMeta
   376  	nested runtime.Object
   377  }
   378  
   379  func (w *wrappedParam) MarshalJSON() ([]byte, error) {
   380  	return nil, errors.New("MarshalJSON unimplemented for wrappedParam")
   381  }
   382  
   383  func (w *wrappedParam) UnmarshalJSON(data []byte) error {
   384  	return errors.New("UnmarshalJSON unimplemented for wrappedParam")
   385  }
   386  
   387  func (w *wrappedParam) ToUnstructured() interface{} {
   388  	res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(w.nested)
   389  
   390  	if err != nil {
   391  		return nil
   392  	}
   393  
   394  	metaRes, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&w.TypeMeta)
   395  	if err != nil {
   396  		return nil
   397  	}
   398  
   399  	for k, v := range metaRes {
   400  		res[k] = v
   401  	}
   402  
   403  	return res
   404  }
   405  
   406  func (w *wrappedParam) DeepCopyObject() runtime.Object {
   407  	return &wrappedParam{
   408  		TypeMeta: w.TypeMeta,
   409  		nested:   w.nested.DeepCopyObject(),
   410  	}
   411  }
   412  
   413  func (w *wrappedParam) GetObjectKind() schema.ObjectKind {
   414  	return w
   415  }