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 }