k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/admissionregistration/validation/validation.go (about) 1 /* 2 Copyright 2017 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 validation 18 19 import ( 20 "fmt" 21 "reflect" 22 "regexp" 23 "strings" 24 25 genericvalidation "k8s.io/apimachinery/pkg/api/validation" 26 "k8s.io/apimachinery/pkg/api/validation/path" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" 29 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 30 "k8s.io/apimachinery/pkg/util/sets" 31 utilvalidation "k8s.io/apimachinery/pkg/util/validation" 32 "k8s.io/apimachinery/pkg/util/validation/field" 33 plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" 34 validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" 35 "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" 36 "k8s.io/apiserver/pkg/cel" 37 "k8s.io/apiserver/pkg/cel/environment" 38 "k8s.io/apiserver/pkg/features" 39 utilfeature "k8s.io/apiserver/pkg/util/feature" 40 "k8s.io/apiserver/pkg/util/webhook" 41 "k8s.io/client-go/util/jsonpath" 42 43 "k8s.io/kubernetes/pkg/apis/admissionregistration" 44 admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1" 45 admissionregistrationv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1" 46 apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" 47 ) 48 49 func hasWildcard(slice []string) bool { 50 for _, s := range slice { 51 if s == "*" { 52 return true 53 } 54 } 55 return false 56 } 57 58 func validateResources(resources []string, fldPath *field.Path) field.ErrorList { 59 var allErrors field.ErrorList 60 if len(resources) == 0 { 61 allErrors = append(allErrors, field.Required(fldPath, "")) 62 } 63 64 // x/* 65 resourcesWithWildcardSubresoures := sets.String{} 66 // */x 67 subResourcesWithWildcardResource := sets.String{} 68 // */* 69 hasDoubleWildcard := false 70 // * 71 hasSingleWildcard := false 72 // x 73 hasResourceWithoutSubresource := false 74 75 for i, resSub := range resources { 76 if resSub == "" { 77 allErrors = append(allErrors, field.Required(fldPath.Index(i), "")) 78 continue 79 } 80 if resSub == "*/*" { 81 hasDoubleWildcard = true 82 } 83 if resSub == "*" { 84 hasSingleWildcard = true 85 } 86 parts := strings.SplitN(resSub, "/", 2) 87 if len(parts) == 1 { 88 hasResourceWithoutSubresource = resSub != "*" 89 continue 90 } 91 res, sub := parts[0], parts[1] 92 if _, ok := resourcesWithWildcardSubresoures[res]; ok { 93 allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resSub, fmt.Sprintf("if '%s/*' is present, must not specify %s", res, resSub))) 94 } 95 if _, ok := subResourcesWithWildcardResource[sub]; ok { 96 allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resSub, fmt.Sprintf("if '*/%s' is present, must not specify %s", sub, resSub))) 97 } 98 if sub == "*" { 99 resourcesWithWildcardSubresoures[res] = struct{}{} 100 } 101 if res == "*" { 102 subResourcesWithWildcardResource[sub] = struct{}{} 103 } 104 } 105 if len(resources) > 1 && hasDoubleWildcard { 106 allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*/*' is present, must not specify other resources")) 107 } 108 if hasSingleWildcard && hasResourceWithoutSubresource { 109 allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*' is present, must not specify other resources without subresources")) 110 } 111 return allErrors 112 } 113 114 func validateResourcesNoSubResources(resources []string, fldPath *field.Path) field.ErrorList { 115 var allErrors field.ErrorList 116 if len(resources) == 0 { 117 allErrors = append(allErrors, field.Required(fldPath, "")) 118 } 119 for i, resource := range resources { 120 if resource == "" { 121 allErrors = append(allErrors, field.Required(fldPath.Index(i), "")) 122 } 123 if strings.Contains(resource, "/") { 124 allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resource, "must not specify subresources")) 125 } 126 } 127 if len(resources) > 1 && hasWildcard(resources) { 128 allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*' is present, must not specify other resources")) 129 } 130 return allErrors 131 } 132 133 var validScopes = sets.NewString( 134 string(admissionregistration.ClusterScope), 135 string(admissionregistration.NamespacedScope), 136 string(admissionregistration.AllScopes), 137 ) 138 139 func validateRule(rule *admissionregistration.Rule, fldPath *field.Path, allowSubResource bool) field.ErrorList { 140 var allErrors field.ErrorList 141 if len(rule.APIGroups) == 0 { 142 allErrors = append(allErrors, field.Required(fldPath.Child("apiGroups"), "")) 143 } 144 if len(rule.APIGroups) > 1 && hasWildcard(rule.APIGroups) { 145 allErrors = append(allErrors, field.Invalid(fldPath.Child("apiGroups"), rule.APIGroups, "if '*' is present, must not specify other API groups")) 146 } 147 // Note: group could be empty, e.g., the legacy "v1" API 148 if len(rule.APIVersions) == 0 { 149 allErrors = append(allErrors, field.Required(fldPath.Child("apiVersions"), "")) 150 } 151 if len(rule.APIVersions) > 1 && hasWildcard(rule.APIVersions) { 152 allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersions"), rule.APIVersions, "if '*' is present, must not specify other API versions")) 153 } 154 for i, version := range rule.APIVersions { 155 if version == "" { 156 allErrors = append(allErrors, field.Required(fldPath.Child("apiVersions").Index(i), "")) 157 } 158 } 159 if allowSubResource { 160 allErrors = append(allErrors, validateResources(rule.Resources, fldPath.Child("resources"))...) 161 } else { 162 allErrors = append(allErrors, validateResourcesNoSubResources(rule.Resources, fldPath.Child("resources"))...) 163 } 164 if rule.Scope != nil && !validScopes.Has(string(*rule.Scope)) { 165 allErrors = append(allErrors, field.NotSupported(fldPath.Child("scope"), *rule.Scope, validScopes.List())) 166 } 167 return allErrors 168 } 169 170 // AcceptedAdmissionReviewVersions contains the list of AdmissionReview versions the *prior* version of the API server understands. 171 // 1.15: server understands v1beta1; accepted versions are ["v1beta1"] 172 // 1.16: server understands v1, v1beta1; accepted versions are ["v1beta1"] 173 // 1.17+: server understands v1, v1beta1; accepted versions are ["v1","v1beta1"] 174 var AcceptedAdmissionReviewVersions = []string{admissionregistrationv1.SchemeGroupVersion.Version, admissionregistrationv1beta1.SchemeGroupVersion.Version} 175 176 func isAcceptedAdmissionReviewVersion(v string) bool { 177 for _, version := range AcceptedAdmissionReviewVersions { 178 if v == version { 179 return true 180 } 181 } 182 return false 183 } 184 185 func validateAdmissionReviewVersions(versions []string, requireRecognizedAdmissionReviewVersion bool, fldPath *field.Path) field.ErrorList { 186 allErrors := field.ErrorList{} 187 188 // Currently only v1beta1 accepted in AdmissionReviewVersions 189 if len(versions) < 1 { 190 allErrors = append(allErrors, field.Required(fldPath, fmt.Sprintf("must specify one of %v", strings.Join(AcceptedAdmissionReviewVersions, ", ")))) 191 } else { 192 seen := map[string]bool{} 193 hasAcceptedVersion := false 194 for i, v := range versions { 195 if seen[v] { 196 allErrors = append(allErrors, field.Invalid(fldPath.Index(i), v, "duplicate version")) 197 continue 198 } 199 seen[v] = true 200 for _, errString := range utilvalidation.IsDNS1035Label(v) { 201 allErrors = append(allErrors, field.Invalid(fldPath.Index(i), v, errString)) 202 } 203 if isAcceptedAdmissionReviewVersion(v) { 204 hasAcceptedVersion = true 205 } 206 } 207 if requireRecognizedAdmissionReviewVersion && !hasAcceptedVersion { 208 allErrors = append(allErrors, field.Invalid( 209 fldPath, versions, 210 fmt.Sprintf("must include at least one of %v", 211 strings.Join(AcceptedAdmissionReviewVersions, ", ")))) 212 } 213 } 214 return allErrors 215 } 216 217 // ValidateValidatingWebhookConfiguration validates a webhook before creation. 218 func ValidateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList { 219 return validateValidatingWebhookConfiguration(e, validationOptions{ 220 ignoreMatchConditions: false, 221 allowParamsInMatchConditions: false, 222 requireNoSideEffects: true, 223 requireRecognizedAdmissionReviewVersion: true, 224 requireUniqueWebhookNames: true, 225 allowInvalidLabelValueInSelector: false, 226 strictCostEnforcement: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks), 227 }) 228 } 229 230 func validateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration, opts validationOptions) field.ErrorList { 231 allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) 232 hookNames := sets.NewString() 233 for i, hook := range e.Webhooks { 234 allErrors = append(allErrors, validateValidatingWebhook(&hook, opts, field.NewPath("webhooks").Index(i))...) 235 allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, opts.requireRecognizedAdmissionReviewVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...) 236 if opts.requireUniqueWebhookNames && len(hook.Name) > 0 { 237 if hookNames.Has(hook.Name) { 238 allErrors = append(allErrors, field.Duplicate(field.NewPath("webhooks").Index(i).Child("name"), hook.Name)) 239 } else { 240 hookNames.Insert(hook.Name) 241 } 242 } 243 } 244 return allErrors 245 } 246 247 // ValidateMutatingWebhookConfiguration validates a webhook before creation. 248 func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration) field.ErrorList { 249 return validateMutatingWebhookConfiguration(e, validationOptions{ 250 ignoreMatchConditions: false, 251 allowParamsInMatchConditions: false, 252 requireNoSideEffects: true, 253 requireRecognizedAdmissionReviewVersion: true, 254 requireUniqueWebhookNames: true, 255 allowInvalidLabelValueInSelector: false, 256 strictCostEnforcement: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks), 257 }) 258 } 259 260 type validationOptions struct { 261 ignoreMatchConditions bool 262 allowParamsInMatchConditions bool 263 requireNoSideEffects bool 264 requireRecognizedAdmissionReviewVersion bool 265 requireUniqueWebhookNames bool 266 allowInvalidLabelValueInSelector bool 267 preexistingExpressions preexistingExpressions 268 strictCostEnforcement bool 269 } 270 271 type preexistingExpressions struct { 272 matchConditionExpressions sets.Set[string] 273 validationExpressions sets.Set[string] 274 validationMessageExpressions sets.Set[string] 275 auditAnnotationValuesExpressions sets.Set[string] 276 } 277 278 func newPreexistingExpressions() preexistingExpressions { 279 return preexistingExpressions{ 280 matchConditionExpressions: sets.New[string](), 281 validationExpressions: sets.New[string](), 282 validationMessageExpressions: sets.New[string](), 283 auditAnnotationValuesExpressions: sets.New[string](), 284 } 285 } 286 287 func findMutatingPreexistingExpressions(mutating *admissionregistration.MutatingWebhookConfiguration) preexistingExpressions { 288 preexisting := newPreexistingExpressions() 289 for _, wh := range mutating.Webhooks { 290 for _, mc := range wh.MatchConditions { 291 preexisting.matchConditionExpressions.Insert(mc.Expression) 292 } 293 } 294 return preexisting 295 } 296 297 func findValidatingPreexistingExpressions(validating *admissionregistration.ValidatingWebhookConfiguration) preexistingExpressions { 298 preexisting := newPreexistingExpressions() 299 for _, wh := range validating.Webhooks { 300 for _, mc := range wh.MatchConditions { 301 preexisting.matchConditionExpressions.Insert(mc.Expression) 302 } 303 } 304 return preexisting 305 } 306 307 func findValidatingPolicyPreexistingExpressions(validatingPolicy *admissionregistration.ValidatingAdmissionPolicy) preexistingExpressions { 308 preexisting := newPreexistingExpressions() 309 for _, mc := range validatingPolicy.Spec.MatchConditions { 310 preexisting.matchConditionExpressions.Insert(mc.Expression) 311 } 312 for _, v := range validatingPolicy.Spec.Validations { 313 preexisting.validationExpressions.Insert(v.Expression) 314 if len(v.MessageExpression) > 0 { 315 preexisting.validationMessageExpressions.Insert(v.MessageExpression) 316 } 317 } 318 for _, a := range validatingPolicy.Spec.AuditAnnotations { 319 preexisting.auditAnnotationValuesExpressions.Insert(a.ValueExpression) 320 } 321 return preexisting 322 } 323 324 func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, opts validationOptions) field.ErrorList { 325 allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) 326 327 hookNames := sets.NewString() 328 for i, hook := range e.Webhooks { 329 allErrors = append(allErrors, validateMutatingWebhook(&hook, opts, field.NewPath("webhooks").Index(i))...) 330 allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, opts.requireRecognizedAdmissionReviewVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...) 331 if opts.requireUniqueWebhookNames && len(hook.Name) > 0 { 332 if hookNames.Has(hook.Name) { 333 allErrors = append(allErrors, field.Duplicate(field.NewPath("webhooks").Index(i).Child("name"), hook.Name)) 334 } else { 335 hookNames.Insert(hook.Name) 336 } 337 } 338 } 339 return allErrors 340 } 341 342 func validateValidatingWebhook(hook *admissionregistration.ValidatingWebhook, opts validationOptions, fldPath *field.Path) field.ErrorList { 343 var allErrors field.ErrorList 344 // hook.Name must be fully qualified 345 allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...) 346 labelSelectorValidationOpts := metav1validation.LabelSelectorValidationOptions{ 347 AllowInvalidLabelValueInSelector: opts.allowInvalidLabelValueInSelector, 348 } 349 350 for i, rule := range hook.Rules { 351 allErrors = append(allErrors, validateRuleWithOperations(&rule, fldPath.Child("rules").Index(i))...) 352 } 353 if hook.FailurePolicy != nil && !supportedFailurePolicies.Has(string(*hook.FailurePolicy)) { 354 allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List())) 355 } 356 if hook.MatchPolicy != nil && !supportedMatchPolicies.Has(string(*hook.MatchPolicy)) { 357 allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *hook.MatchPolicy, supportedMatchPolicies.List())) 358 } 359 allowedSideEffects := supportedSideEffectClasses 360 if opts.requireNoSideEffects { 361 allowedSideEffects = noSideEffectClasses 362 } 363 if hook.SideEffects == nil { 364 allErrors = append(allErrors, field.Required(fldPath.Child("sideEffects"), fmt.Sprintf("must specify one of %v", strings.Join(allowedSideEffects.List(), ", ")))) 365 } 366 if hook.SideEffects != nil && !allowedSideEffects.Has(string(*hook.SideEffects)) { 367 allErrors = append(allErrors, field.NotSupported(fldPath.Child("sideEffects"), *hook.SideEffects, allowedSideEffects.List())) 368 } 369 if hook.TimeoutSeconds != nil && (*hook.TimeoutSeconds > 30 || *hook.TimeoutSeconds < 1) { 370 allErrors = append(allErrors, field.Invalid(fldPath.Child("timeoutSeconds"), *hook.TimeoutSeconds, "the timeout value must be between 1 and 30 seconds")) 371 } 372 373 if hook.NamespaceSelector != nil { 374 allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, labelSelectorValidationOpts, fldPath.Child("namespaceSelector"))...) 375 } 376 377 if hook.ObjectSelector != nil { 378 allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.ObjectSelector, labelSelectorValidationOpts, fldPath.Child("objectSelector"))...) 379 } 380 381 cc := hook.ClientConfig 382 switch { 383 case (cc.URL == nil) == (cc.Service == nil): 384 allErrors = append(allErrors, field.Required(fldPath.Child("clientConfig"), "exactly one of url or service is required")) 385 case cc.URL != nil: 386 allErrors = append(allErrors, webhook.ValidateWebhookURL(fldPath.Child("clientConfig").Child("url"), *cc.URL, true)...) 387 case cc.Service != nil: 388 allErrors = append(allErrors, webhook.ValidateWebhookService(fldPath.Child("clientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, cc.Service.Port)...) 389 } 390 391 if !opts.ignoreMatchConditions { 392 allErrors = append(allErrors, validateMatchConditions(hook.MatchConditions, opts, fldPath.Child("matchConditions"))...) 393 } 394 395 return allErrors 396 } 397 398 func validateMutatingWebhook(hook *admissionregistration.MutatingWebhook, opts validationOptions, fldPath *field.Path) field.ErrorList { 399 var allErrors field.ErrorList 400 // hook.Name must be fully qualified 401 allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...) 402 labelSelectorValidationOpts := metav1validation.LabelSelectorValidationOptions{ 403 AllowInvalidLabelValueInSelector: opts.allowInvalidLabelValueInSelector, 404 } 405 406 for i, rule := range hook.Rules { 407 allErrors = append(allErrors, validateRuleWithOperations(&rule, fldPath.Child("rules").Index(i))...) 408 } 409 if hook.FailurePolicy != nil && !supportedFailurePolicies.Has(string(*hook.FailurePolicy)) { 410 allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List())) 411 } 412 if hook.MatchPolicy != nil && !supportedMatchPolicies.Has(string(*hook.MatchPolicy)) { 413 allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *hook.MatchPolicy, supportedMatchPolicies.List())) 414 } 415 allowedSideEffects := supportedSideEffectClasses 416 if opts.requireNoSideEffects { 417 allowedSideEffects = noSideEffectClasses 418 } 419 if hook.SideEffects == nil { 420 allErrors = append(allErrors, field.Required(fldPath.Child("sideEffects"), fmt.Sprintf("must specify one of %v", strings.Join(allowedSideEffects.List(), ", ")))) 421 } 422 if hook.SideEffects != nil && !allowedSideEffects.Has(string(*hook.SideEffects)) { 423 allErrors = append(allErrors, field.NotSupported(fldPath.Child("sideEffects"), *hook.SideEffects, allowedSideEffects.List())) 424 } 425 if hook.TimeoutSeconds != nil && (*hook.TimeoutSeconds > 30 || *hook.TimeoutSeconds < 1) { 426 allErrors = append(allErrors, field.Invalid(fldPath.Child("timeoutSeconds"), *hook.TimeoutSeconds, "the timeout value must be between 1 and 30 seconds")) 427 } 428 429 if hook.NamespaceSelector != nil { 430 allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, labelSelectorValidationOpts, fldPath.Child("namespaceSelector"))...) 431 } 432 if hook.ObjectSelector != nil { 433 allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.ObjectSelector, labelSelectorValidationOpts, fldPath.Child("objectSelector"))...) 434 } 435 if hook.ReinvocationPolicy != nil && !supportedReinvocationPolicies.Has(string(*hook.ReinvocationPolicy)) { 436 allErrors = append(allErrors, field.NotSupported(fldPath.Child("reinvocationPolicy"), *hook.ReinvocationPolicy, supportedReinvocationPolicies.List())) 437 } 438 439 cc := hook.ClientConfig 440 switch { 441 case (cc.URL == nil) == (cc.Service == nil): 442 allErrors = append(allErrors, field.Required(fldPath.Child("clientConfig"), "exactly one of url or service is required")) 443 case cc.URL != nil: 444 allErrors = append(allErrors, webhook.ValidateWebhookURL(fldPath.Child("clientConfig").Child("url"), *cc.URL, true)...) 445 case cc.Service != nil: 446 allErrors = append(allErrors, webhook.ValidateWebhookService(fldPath.Child("clientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, cc.Service.Port)...) 447 } 448 449 if !opts.ignoreMatchConditions { 450 allErrors = append(allErrors, validateMatchConditions(hook.MatchConditions, opts, fldPath.Child("matchConditions"))...) 451 } 452 453 return allErrors 454 } 455 456 var supportedFailurePolicies = sets.NewString( 457 string(admissionregistration.Ignore), 458 string(admissionregistration.Fail), 459 ) 460 461 var supportedMatchPolicies = sets.NewString( 462 string(admissionregistration.Exact), 463 string(admissionregistration.Equivalent), 464 ) 465 466 var supportedSideEffectClasses = sets.NewString( 467 string(admissionregistration.SideEffectClassUnknown), 468 string(admissionregistration.SideEffectClassNone), 469 string(admissionregistration.SideEffectClassSome), 470 string(admissionregistration.SideEffectClassNoneOnDryRun), 471 ) 472 473 var noSideEffectClasses = sets.NewString( 474 string(admissionregistration.SideEffectClassNone), 475 string(admissionregistration.SideEffectClassNoneOnDryRun), 476 ) 477 478 var supportedOperations = sets.NewString( 479 string(admissionregistration.OperationAll), 480 string(admissionregistration.Create), 481 string(admissionregistration.Update), 482 string(admissionregistration.Delete), 483 string(admissionregistration.Connect), 484 ) 485 486 var supportedReinvocationPolicies = sets.NewString( 487 string(admissionregistration.NeverReinvocationPolicy), 488 string(admissionregistration.IfNeededReinvocationPolicy), 489 ) 490 491 var supportedValidationPolicyReason = sets.NewString( 492 string(metav1.StatusReasonForbidden), 493 string(metav1.StatusReasonInvalid), 494 string(metav1.StatusReasonRequestEntityTooLarge), 495 ) 496 497 func hasWildcardOperation(operations []admissionregistration.OperationType) bool { 498 for _, o := range operations { 499 if o == admissionregistration.OperationAll { 500 return true 501 } 502 } 503 return false 504 } 505 506 func validateRuleWithOperations(ruleWithOperations *admissionregistration.RuleWithOperations, fldPath *field.Path) field.ErrorList { 507 var allErrors field.ErrorList 508 if len(ruleWithOperations.Operations) == 0 { 509 allErrors = append(allErrors, field.Required(fldPath.Child("operations"), "")) 510 } 511 if len(ruleWithOperations.Operations) > 1 && hasWildcardOperation(ruleWithOperations.Operations) { 512 allErrors = append(allErrors, field.Invalid(fldPath.Child("operations"), ruleWithOperations.Operations, "if '*' is present, must not specify other operations")) 513 } 514 for i, operation := range ruleWithOperations.Operations { 515 if !supportedOperations.Has(string(operation)) { 516 allErrors = append(allErrors, field.NotSupported(fldPath.Child("operations").Index(i), operation, supportedOperations.List())) 517 } 518 } 519 allowSubResource := true 520 allErrors = append(allErrors, validateRule(&ruleWithOperations.Rule, fldPath, allowSubResource)...) 521 return allErrors 522 } 523 524 // mutatingHasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one 525 // admission review version this apiserver accepts. 526 func mutatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.MutatingWebhook) bool { 527 for _, hook := range webhooks { 528 hasRecognizedVersion := false 529 for _, version := range hook.AdmissionReviewVersions { 530 if isAcceptedAdmissionReviewVersion(version) { 531 hasRecognizedVersion = true 532 break 533 } 534 } 535 if !hasRecognizedVersion && len(hook.AdmissionReviewVersions) > 0 { 536 return false 537 } 538 } 539 return true 540 } 541 542 // validatingHasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one 543 // admission review version this apiserver accepts. 544 func validatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.ValidatingWebhook) bool { 545 for _, hook := range webhooks { 546 hasRecognizedVersion := false 547 for _, version := range hook.AdmissionReviewVersions { 548 if isAcceptedAdmissionReviewVersion(version) { 549 hasRecognizedVersion = true 550 break 551 } 552 } 553 if !hasRecognizedVersion && len(hook.AdmissionReviewVersions) > 0 { 554 return false 555 } 556 } 557 return true 558 } 559 560 // ignoreMatchConditions returns false if any change to match conditions 561 func ignoreMutatingWebhookMatchConditions(new, old []admissionregistration.MutatingWebhook) bool { 562 if len(new) != len(old) { 563 return false 564 } 565 for i := range old { 566 if !reflect.DeepEqual(new[i].MatchConditions, old[i].MatchConditions) { 567 return false 568 } 569 } 570 571 return true 572 } 573 574 // ignoreMatchConditions returns true if any new expressions are added 575 func ignoreValidatingWebhookMatchConditions(new, old []admissionregistration.ValidatingWebhook) bool { 576 if len(new) != len(old) { 577 return false 578 } 579 for i := range old { 580 if !reflect.DeepEqual(new[i].MatchConditions, old[i].MatchConditions) { 581 return false 582 } 583 } 584 585 return true 586 } 587 588 // ignoreValidatingAdmissionPolicyMatchConditions returns true if there have been no updates that could invalidate previously-valid match conditions 589 func ignoreValidatingAdmissionPolicyMatchConditions(new, old *admissionregistration.ValidatingAdmissionPolicy) bool { 590 if !reflect.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) { 591 return false 592 } 593 if !reflect.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) { 594 return false 595 } 596 return true 597 } 598 599 // mutatingHasUniqueWebhookNames returns true if all webhooks have unique names 600 func mutatingHasUniqueWebhookNames(webhooks []admissionregistration.MutatingWebhook) bool { 601 names := sets.NewString() 602 for _, hook := range webhooks { 603 if names.Has(hook.Name) { 604 return false 605 } 606 names.Insert(hook.Name) 607 } 608 return true 609 } 610 611 // validatingHasUniqueWebhookNames returns true if all webhooks have unique names 612 func validatingHasUniqueWebhookNames(webhooks []admissionregistration.ValidatingWebhook) bool { 613 names := sets.NewString() 614 for _, hook := range webhooks { 615 if names.Has(hook.Name) { 616 return false 617 } 618 names.Insert(hook.Name) 619 } 620 return true 621 } 622 623 // mutatingHasNoSideEffects returns true if all webhooks have no side effects 624 func mutatingHasNoSideEffects(webhooks []admissionregistration.MutatingWebhook) bool { 625 for _, hook := range webhooks { 626 if hook.SideEffects == nil || !noSideEffectClasses.Has(string(*hook.SideEffects)) { 627 return false 628 } 629 } 630 return true 631 } 632 633 // validatingHasNoSideEffects returns true if all webhooks have no side effects 634 func validatingHasNoSideEffects(webhooks []admissionregistration.ValidatingWebhook) bool { 635 for _, hook := range webhooks { 636 if hook.SideEffects == nil || !noSideEffectClasses.Has(string(*hook.SideEffects)) { 637 return false 638 } 639 } 640 return true 641 } 642 643 // validatingWebhookAllowInvalidLabelValueInSelector returns true if all webhooksallow invalid label value in selector 644 func validatingWebhookHasInvalidLabelValueInSelector(webhooks []admissionregistration.ValidatingWebhook) bool { 645 labelSelectorValidationOpts := metav1validation.LabelSelectorValidationOptions{ 646 AllowInvalidLabelValueInSelector: false, 647 } 648 649 for _, hook := range webhooks { 650 if hook.NamespaceSelector != nil { 651 if len(metav1validation.ValidateLabelSelector(hook.NamespaceSelector, labelSelectorValidationOpts, nil)) > 0 { 652 return true 653 } 654 } 655 if hook.ObjectSelector != nil { 656 if len(metav1validation.ValidateLabelSelector(hook.ObjectSelector, labelSelectorValidationOpts, nil)) > 0 { 657 return true 658 } 659 } 660 } 661 return false 662 } 663 664 // mutatingWebhookAllowInvalidLabelValueInSelector returns true if all webhooks allow invalid label value in selector 665 func mutatingWebhookHasInvalidLabelValueInSelector(webhooks []admissionregistration.MutatingWebhook) bool { 666 labelSelectorValidationOpts := metav1validation.LabelSelectorValidationOptions{ 667 AllowInvalidLabelValueInSelector: false, 668 } 669 670 for _, hook := range webhooks { 671 if hook.NamespaceSelector != nil { 672 if len(metav1validation.ValidateLabelSelector(hook.NamespaceSelector, labelSelectorValidationOpts, nil)) > 0 { 673 return true 674 } 675 } 676 if hook.ObjectSelector != nil { 677 if len(metav1validation.ValidateLabelSelector(hook.ObjectSelector, labelSelectorValidationOpts, nil)) > 0 { 678 return true 679 } 680 } 681 } 682 return false 683 } 684 685 // ValidateValidatingWebhookConfigurationUpdate validates update of validating webhook configuration 686 func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList { 687 return validateValidatingWebhookConfiguration(newC, validationOptions{ 688 ignoreMatchConditions: ignoreValidatingWebhookMatchConditions(newC.Webhooks, oldC.Webhooks), 689 allowParamsInMatchConditions: false, 690 requireNoSideEffects: validatingHasNoSideEffects(oldC.Webhooks), 691 requireRecognizedAdmissionReviewVersion: validatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks), 692 requireUniqueWebhookNames: validatingHasUniqueWebhookNames(oldC.Webhooks), 693 allowInvalidLabelValueInSelector: validatingWebhookHasInvalidLabelValueInSelector(oldC.Webhooks), 694 preexistingExpressions: findValidatingPreexistingExpressions(oldC), 695 strictCostEnforcement: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks), 696 }) 697 } 698 699 // ValidateMutatingWebhookConfigurationUpdate validates update of mutating webhook configuration 700 func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.MutatingWebhookConfiguration) field.ErrorList { 701 return validateMutatingWebhookConfiguration(newC, validationOptions{ 702 ignoreMatchConditions: ignoreMutatingWebhookMatchConditions(newC.Webhooks, oldC.Webhooks), 703 allowParamsInMatchConditions: false, 704 requireNoSideEffects: mutatingHasNoSideEffects(oldC.Webhooks), 705 requireRecognizedAdmissionReviewVersion: mutatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks), 706 requireUniqueWebhookNames: mutatingHasUniqueWebhookNames(oldC.Webhooks), 707 allowInvalidLabelValueInSelector: mutatingWebhookHasInvalidLabelValueInSelector(oldC.Webhooks), 708 preexistingExpressions: findMutatingPreexistingExpressions(oldC), 709 strictCostEnforcement: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks), 710 }) 711 } 712 713 const ( 714 maxAuditAnnotations = 20 715 // use a 5kb limit the CEL expression, note that this is less than the length limit 716 // for the audit annotation value limit (10kb) since an expressions that concatenates 717 // strings will often produce a longer value than the expression 718 maxAuditAnnotationValueExpressionLength = 5 * 1024 719 ) 720 721 // ValidateValidatingAdmissionPolicy validates a ValidatingAdmissionPolicy before creation. 722 func ValidateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList { 723 return validateValidatingAdmissionPolicy(p, validationOptions{ignoreMatchConditions: false, strictCostEnforcement: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP)}) 724 } 725 726 func validateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy, opts validationOptions) field.ErrorList { 727 allErrors := genericvalidation.ValidateObjectMeta(&p.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) 728 allErrors = append(allErrors, validateValidatingAdmissionPolicySpec(p.ObjectMeta, &p.Spec, opts, field.NewPath("spec"))...) 729 return allErrors 730 } 731 732 func validateValidatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissionregistration.ValidatingAdmissionPolicySpec, opts validationOptions, fldPath *field.Path) field.ErrorList { 733 var allErrors field.ErrorList 734 var compiler plugincel.Compiler // composition compiler is stateful, create one lazily per policy 735 getCompiler := func() plugincel.Compiler { 736 if compiler == nil { 737 needsComposition := len(spec.Variables) > 0 738 compiler = createCompiler(needsComposition, opts.strictCostEnforcement) 739 } 740 return compiler 741 } 742 if spec.FailurePolicy == nil { 743 allErrors = append(allErrors, field.Required(fldPath.Child("failurePolicy"), "")) 744 } else if !supportedFailurePolicies.Has(string(*spec.FailurePolicy)) { 745 allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *spec.FailurePolicy, supportedFailurePolicies.List())) 746 } 747 if spec.ParamKind != nil { 748 opts.allowParamsInMatchConditions = true 749 allErrors = append(allErrors, validateParamKind(*spec.ParamKind, fldPath.Child("paramKind"))...) 750 } 751 if spec.MatchConstraints == nil { 752 allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints"), "")) 753 } else { 754 allErrors = append(allErrors, validateMatchResources(spec.MatchConstraints, fldPath.Child("matchConstraints"))...) 755 // at least one resourceRule must be defined to provide type information 756 if len(spec.MatchConstraints.ResourceRules) == 0 { 757 allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints", "resourceRules"), "")) 758 } 759 } 760 if !opts.ignoreMatchConditions { 761 allErrors = append(allErrors, validateMatchConditions(spec.MatchConditions, opts, fldPath.Child("matchConditions"))...) 762 } 763 if len(spec.Variables) > 0 { 764 for i, variable := range spec.Variables { 765 allErrors = append(allErrors, validateVariable(getCompiler(), &variable, spec.ParamKind, opts, fldPath.Child("variables").Index(i))...) 766 } 767 } 768 if len(spec.Validations) == 0 && len(spec.AuditAnnotations) == 0 { 769 allErrors = append(allErrors, field.Required(fldPath.Child("validations"), "validations or auditAnnotations must contain at least one item")) 770 allErrors = append(allErrors, field.Required(fldPath.Child("auditAnnotations"), "validations or auditAnnotations must contain at least one item")) 771 } else { 772 for i, validation := range spec.Validations { 773 allErrors = append(allErrors, validateValidation(getCompiler(), &validation, spec.ParamKind, opts, fldPath.Child("validations").Index(i))...) 774 } 775 if spec.AuditAnnotations != nil { 776 keys := sets.NewString() 777 if len(spec.AuditAnnotations) > maxAuditAnnotations { 778 allErrors = append(allErrors, field.Invalid(fldPath.Child("auditAnnotations"), spec.AuditAnnotations, fmt.Sprintf("must not have more than %d auditAnnotations", maxAuditAnnotations))) 779 } 780 for i, auditAnnotation := range spec.AuditAnnotations { 781 allErrors = append(allErrors, validateAuditAnnotation(getCompiler(), meta, &auditAnnotation, spec.ParamKind, opts, fldPath.Child("auditAnnotations").Index(i))...) 782 if keys.Has(auditAnnotation.Key) { 783 allErrors = append(allErrors, field.Duplicate(fldPath.Child("auditAnnotations").Index(i).Child("key"), auditAnnotation.Key)) 784 } 785 keys.Insert(auditAnnotation.Key) 786 } 787 } 788 } 789 return allErrors 790 } 791 792 func validateParamKind(gvk admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList { 793 var allErrors field.ErrorList 794 if len(gvk.APIVersion) == 0 { 795 allErrors = append(allErrors, field.Required(fldPath.Child("apiVersion"), "")) 796 } else if gv, err := parseGroupVersion(gvk.APIVersion); err != nil { 797 allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gvk.APIVersion, err.Error())) 798 } else { 799 // this matches the APIService group field validation 800 if len(gv.Group) > 0 { 801 if errs := utilvalidation.IsDNS1123Subdomain(gv.Group); len(errs) > 0 { 802 allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gv.Group, strings.Join(errs, ","))) 803 } 804 } 805 // this matches the APIService version field validation 806 if len(gv.Version) == 0 { 807 allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gvk.APIVersion, "version must be specified")) 808 } else { 809 if errs := utilvalidation.IsDNS1035Label(gv.Version); len(errs) > 0 { 810 allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gv.Version, strings.Join(errs, ","))) 811 } 812 } 813 } 814 if len(gvk.Kind) == 0 { 815 allErrors = append(allErrors, field.Required(fldPath.Child("kind"), "")) 816 } else if errs := utilvalidation.IsDNS1035Label(strings.ToLower(gvk.Kind)); len(errs) > 0 { 817 allErrors = append(allErrors, field.Invalid(fldPath.Child("kind"), gvk.Kind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ","))) 818 } 819 820 return allErrors 821 } 822 823 type groupVersion struct { 824 Group string 825 Version string 826 } 827 828 // parseGroupVersion turns "group/version" string into a groupVersion struct. It reports error 829 // if it cannot parse the string. 830 func parseGroupVersion(gv string) (groupVersion, error) { 831 if (len(gv) == 0) || (gv == "/") { 832 return groupVersion{}, nil 833 } 834 835 switch strings.Count(gv, "/") { 836 case 0: 837 return groupVersion{"", gv}, nil 838 case 1: 839 i := strings.Index(gv, "/") 840 return groupVersion{gv[:i], gv[i+1:]}, nil 841 default: 842 return groupVersion{}, fmt.Errorf("unexpected GroupVersion string: %v", gv) 843 } 844 } 845 846 func validateMatchResources(mc *admissionregistration.MatchResources, fldPath *field.Path) field.ErrorList { 847 var allErrors field.ErrorList 848 if mc == nil { 849 return allErrors 850 } 851 if mc.MatchPolicy == nil { 852 allErrors = append(allErrors, field.Required(fldPath.Child("matchPolicy"), "")) 853 } else if !supportedMatchPolicies.Has(string(*mc.MatchPolicy)) { 854 allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *mc.MatchPolicy, supportedMatchPolicies.List())) 855 } 856 if mc.NamespaceSelector == nil { 857 allErrors = append(allErrors, field.Required(fldPath.Child("namespaceSelector"), "")) 858 } else { 859 // validate selector strictly, this type was released after issue #99139 was resolved 860 allErrors = append(allErrors, metav1validation.ValidateLabelSelector(mc.NamespaceSelector, metav1validation.LabelSelectorValidationOptions{}, fldPath.Child("namespaceSelector"))...) 861 } 862 863 if mc.ObjectSelector == nil { 864 allErrors = append(allErrors, field.Required(fldPath.Child("objectSelector"), "")) 865 } else { 866 // validate selector strictly, this type was released after issue #99139 was resolved 867 allErrors = append(allErrors, metav1validation.ValidateLabelSelector(mc.ObjectSelector, metav1validation.LabelSelectorValidationOptions{}, fldPath.Child("objectSelector"))...) 868 } 869 870 for i, namedRuleWithOperations := range mc.ResourceRules { 871 allErrors = append(allErrors, validateNamedRuleWithOperations(&namedRuleWithOperations, fldPath.Child("resourceRules").Index(i))...) 872 } 873 874 for i, namedRuleWithOperations := range mc.ExcludeResourceRules { 875 allErrors = append(allErrors, validateNamedRuleWithOperations(&namedRuleWithOperations, fldPath.Child("excludeResourceRules").Index(i))...) 876 } 877 return allErrors 878 } 879 880 var validValidationActions = sets.NewString( 881 string(admissionregistration.Deny), 882 string(admissionregistration.Warn), 883 string(admissionregistration.Audit), 884 ) 885 886 func validateValidationActions(va []admissionregistration.ValidationAction, fldPath *field.Path) field.ErrorList { 887 var allErrors field.ErrorList 888 actions := sets.NewString() 889 for i, action := range va { 890 if !validValidationActions.Has(string(action)) { 891 allErrors = append(allErrors, field.NotSupported(fldPath.Index(i), action, validValidationActions.List())) 892 } 893 if actions.Has(string(action)) { 894 allErrors = append(allErrors, field.Duplicate(fldPath.Index(i), action)) 895 } 896 actions.Insert(string(action)) 897 } 898 if actions.Has(string(admissionregistration.Deny)) && actions.Has(string(admissionregistration.Warn)) { 899 allErrors = append(allErrors, field.Invalid(fldPath, va, "must not contain both Deny and Warn (repeating the same validation failure information in the API response and headers serves no purpose)")) 900 } 901 if len(actions) == 0 { 902 allErrors = append(allErrors, field.Required(fldPath, "at least one validation action is required")) 903 } 904 return allErrors 905 } 906 907 func validateNamedRuleWithOperations(n *admissionregistration.NamedRuleWithOperations, fldPath *field.Path) field.ErrorList { 908 var allErrors field.ErrorList 909 resourceNames := sets.NewString() 910 for i, rName := range n.ResourceNames { 911 for _, msg := range path.ValidatePathSegmentName(rName, false) { 912 allErrors = append(allErrors, field.Invalid(fldPath.Child("resourceNames").Index(i), rName, msg)) 913 } 914 if resourceNames.Has(rName) { 915 allErrors = append(allErrors, field.Duplicate(fldPath.Child("resourceNames").Index(i), rName)) 916 } else { 917 resourceNames.Insert(rName) 918 } 919 } 920 allErrors = append(allErrors, validateRuleWithOperations(&n.RuleWithOperations, fldPath)...) 921 return allErrors 922 } 923 924 func validateMatchConditions(m []admissionregistration.MatchCondition, opts validationOptions, fldPath *field.Path) field.ErrorList { 925 var allErrors field.ErrorList 926 conditionNames := sets.NewString() 927 if len(m) > 64 { 928 allErrors = append(allErrors, field.TooMany(fldPath, len(m), 64)) 929 } 930 for i, matchCondition := range m { 931 allErrors = append(allErrors, validateMatchCondition(&matchCondition, opts, fldPath.Index(i))...) 932 if len(matchCondition.Name) > 0 { 933 if conditionNames.Has(matchCondition.Name) { 934 allErrors = append(allErrors, field.Duplicate(fldPath.Index(i).Child("name"), matchCondition.Name)) 935 } else { 936 conditionNames.Insert(matchCondition.Name) 937 } 938 } 939 } 940 return allErrors 941 } 942 943 func validateMatchCondition(v *admissionregistration.MatchCondition, opts validationOptions, fldPath *field.Path) field.ErrorList { 944 var allErrors field.ErrorList 945 trimmedExpression := strings.TrimSpace(v.Expression) 946 if len(trimmedExpression) == 0 { 947 allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "")) 948 } else { 949 allErrors = append(allErrors, validateMatchConditionsExpression(trimmedExpression, opts, fldPath.Child("expression"))...) 950 } 951 if len(v.Name) == 0 { 952 allErrors = append(allErrors, field.Required(fldPath.Child("name"), "")) 953 } else { 954 allErrors = append(allErrors, apivalidation.ValidateQualifiedName(v.Name, fldPath.Child("name"))...) 955 } 956 return allErrors 957 } 958 959 func validateVariable(compiler plugincel.Compiler, v *admissionregistration.Variable, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList { 960 var allErrors field.ErrorList 961 if len(v.Name) == 0 || strings.TrimSpace(v.Name) == "" { 962 allErrors = append(allErrors, field.Required(fldPath.Child("name"), "name is not specified")) 963 } else { 964 if !isCELIdentifier(v.Name) { 965 allErrors = append(allErrors, field.Invalid(fldPath.Child("name"), v.Name, "name is not a valid CEL identifier")) 966 } 967 } 968 if len(v.Expression) == 0 || strings.TrimSpace(v.Expression) == "" { 969 allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified")) 970 } else { 971 if compiler, ok := compiler.(*plugincel.CompositedCompiler); ok { 972 envType := environment.NewExpressions 973 if opts.preexistingExpressions.validationExpressions.Has(v.Expression) { 974 envType = environment.StoredExpressions 975 } 976 variable := &validatingadmissionpolicy.Variable{ 977 Name: v.Name, 978 Expression: v.Expression, 979 } 980 result := compiler.CompileAndStoreVariable(variable, plugincel.OptionalVariableDeclarations{ 981 HasParams: paramKind != nil, 982 HasAuthorizer: true, 983 StrictCost: opts.strictCostEnforcement, 984 }, envType) 985 if result.Error != nil { 986 allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), variable, result.Error)) 987 } 988 } else { 989 allErrors = append(allErrors, field.InternalError(fldPath, fmt.Errorf("variable composition is not allowed"))) 990 } 991 } 992 return allErrors 993 } 994 995 func validateValidation(compiler plugincel.Compiler, v *admissionregistration.Validation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList { 996 var allErrors field.ErrorList 997 trimmedExpression := strings.TrimSpace(v.Expression) 998 trimmedMsg := strings.TrimSpace(v.Message) 999 trimmedMessageExpression := strings.TrimSpace(v.MessageExpression) 1000 if len(trimmedExpression) == 0 { 1001 allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified")) 1002 } else { 1003 allErrors = append(allErrors, validateValidationExpression(compiler, v.Expression, paramKind != nil, opts, fldPath.Child("expression"))...) 1004 } 1005 if len(v.MessageExpression) > 0 && len(trimmedMessageExpression) == 0 { 1006 allErrors = append(allErrors, field.Invalid(fldPath.Child("messageExpression"), v.MessageExpression, "must be non-empty if specified")) 1007 } else if len(trimmedMessageExpression) != 0 { 1008 // use v.MessageExpression instead of trimmedMessageExpression so that 1009 // the compiler output shows the correct column. 1010 allErrors = append(allErrors, validateMessageExpression(compiler, v.MessageExpression, opts, fldPath.Child("messageExpression"))...) 1011 } 1012 if len(v.Message) > 0 && len(trimmedMsg) == 0 { 1013 allErrors = append(allErrors, field.Invalid(fldPath.Child("message"), v.Message, "message must be non-empty if specified")) 1014 } else if hasNewlines(trimmedMsg) { 1015 allErrors = append(allErrors, field.Invalid(fldPath.Child("message"), v.Message, "message must not contain line breaks")) 1016 } else if hasNewlines(trimmedMsg) && trimmedMsg == "" { 1017 allErrors = append(allErrors, field.Required(fldPath.Child("message"), "message must be specified if expression contains line breaks")) 1018 } 1019 if v.Reason != nil && !supportedValidationPolicyReason.Has(string(*v.Reason)) { 1020 allErrors = append(allErrors, field.NotSupported(fldPath.Child("reason"), *v.Reason, supportedValidationPolicyReason.List())) 1021 } 1022 return allErrors 1023 } 1024 1025 func validateCELCondition(compiler plugincel.Compiler, expression plugincel.ExpressionAccessor, variables plugincel.OptionalVariableDeclarations, envType environment.Type, fldPath *field.Path) field.ErrorList { 1026 var allErrors field.ErrorList 1027 result := compiler.CompileCELExpression(expression, variables, envType) 1028 if result.Error != nil { 1029 allErrors = append(allErrors, convertCELErrorToValidationError(fldPath, expression, result.Error)) 1030 } 1031 return allErrors 1032 } 1033 1034 func convertCELErrorToValidationError(fldPath *field.Path, expression plugincel.ExpressionAccessor, err error) *field.Error { 1035 if celErr, ok := err.(*cel.Error); ok { 1036 switch celErr.Type { 1037 case cel.ErrorTypeRequired: 1038 return field.Required(fldPath, celErr.Detail) 1039 case cel.ErrorTypeInvalid: 1040 return field.Invalid(fldPath, expression.GetExpression(), celErr.Detail) 1041 case cel.ErrorTypeInternal: 1042 return field.InternalError(fldPath, celErr) 1043 } 1044 } 1045 return field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", err)) 1046 } 1047 1048 func validateValidationExpression(compiler plugincel.Compiler, expression string, hasParams bool, opts validationOptions, fldPath *field.Path) field.ErrorList { 1049 envType := environment.NewExpressions 1050 if opts.preexistingExpressions.validationExpressions.Has(expression) { 1051 envType = environment.StoredExpressions 1052 } 1053 return validateCELCondition(compiler, &validatingadmissionpolicy.ValidationCondition{ 1054 Expression: expression, 1055 }, plugincel.OptionalVariableDeclarations{ 1056 HasParams: hasParams, 1057 HasAuthorizer: true, 1058 StrictCost: opts.strictCostEnforcement, 1059 }, envType, fldPath) 1060 } 1061 1062 func validateMatchConditionsExpression(expression string, opts validationOptions, fldPath *field.Path) field.ErrorList { 1063 envType := environment.NewExpressions 1064 if opts.preexistingExpressions.matchConditionExpressions.Has(expression) { 1065 envType = environment.StoredExpressions 1066 } 1067 var compiler plugincel.Compiler 1068 if opts.strictCostEnforcement { 1069 compiler = strictStatelessCELCompiler 1070 } else { 1071 compiler = nonStrictStatelessCELCompiler 1072 } 1073 return validateCELCondition(compiler, &matchconditions.MatchCondition{ 1074 Expression: expression, 1075 }, plugincel.OptionalVariableDeclarations{ 1076 HasParams: opts.allowParamsInMatchConditions, 1077 HasAuthorizer: true, 1078 StrictCost: opts.strictCostEnforcement, 1079 }, envType, fldPath) 1080 } 1081 1082 func validateMessageExpression(compiler plugincel.Compiler, expression string, opts validationOptions, fldPath *field.Path) field.ErrorList { 1083 envType := environment.NewExpressions 1084 if opts.preexistingExpressions.validationMessageExpressions.Has(expression) { 1085 envType = environment.StoredExpressions 1086 } 1087 return validateCELCondition(compiler, &validatingadmissionpolicy.MessageExpressionCondition{ 1088 MessageExpression: expression, 1089 }, plugincel.OptionalVariableDeclarations{ 1090 HasParams: opts.allowParamsInMatchConditions, 1091 HasAuthorizer: false, 1092 StrictCost: opts.strictCostEnforcement, 1093 }, envType, fldPath) 1094 } 1095 1096 func validateAuditAnnotation(compiler plugincel.Compiler, meta metav1.ObjectMeta, v *admissionregistration.AuditAnnotation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList { 1097 var allErrors field.ErrorList 1098 if len(meta.GetName()) != 0 { 1099 name := meta.GetName() 1100 allErrors = append(allErrors, apivalidation.ValidateQualifiedName(name+"/"+v.Key, fldPath.Child("key"))...) 1101 } else { 1102 allErrors = append(allErrors, field.Invalid(fldPath.Child("key"), v.Key, "requires metadata.name be non-empty")) 1103 } 1104 1105 trimmedValueExpression := strings.TrimSpace(v.ValueExpression) 1106 if len(trimmedValueExpression) == 0 { 1107 allErrors = append(allErrors, field.Required(fldPath.Child("valueExpression"), "valueExpression is not specified")) 1108 } else if len(trimmedValueExpression) > maxAuditAnnotationValueExpressionLength { 1109 allErrors = append(allErrors, field.Required(fldPath.Child("valueExpression"), fmt.Sprintf("must not exceed %d bytes in length", maxAuditAnnotationValueExpressionLength))) 1110 } else { 1111 envType := environment.NewExpressions 1112 if opts.preexistingExpressions.auditAnnotationValuesExpressions.Has(v.ValueExpression) { 1113 envType = environment.StoredExpressions 1114 } 1115 result := compiler.CompileCELExpression(&validatingadmissionpolicy.AuditAnnotationCondition{ 1116 ValueExpression: trimmedValueExpression, 1117 }, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: opts.strictCostEnforcement}, envType) 1118 if result.Error != nil { 1119 switch result.Error.Type { 1120 case cel.ErrorTypeRequired: 1121 allErrors = append(allErrors, field.Required(fldPath.Child("valueExpression"), result.Error.Detail)) 1122 case cel.ErrorTypeInvalid: 1123 allErrors = append(allErrors, field.Invalid(fldPath.Child("valueExpression"), v.ValueExpression, result.Error.Detail)) 1124 default: 1125 allErrors = append(allErrors, field.InternalError(fldPath.Child("valueExpression"), result.Error)) 1126 } 1127 } 1128 } 1129 return allErrors 1130 } 1131 1132 var newlineMatcher = regexp.MustCompile(`[\n\r]+`) // valid newline chars in CEL grammar 1133 func hasNewlines(s string) bool { 1134 return newlineMatcher.MatchString(s) 1135 } 1136 1137 // ValidateValidatingAdmissionPolicyBinding validates a ValidatingAdmissionPolicyBinding before create. 1138 func ValidateValidatingAdmissionPolicyBinding(pb *admissionregistration.ValidatingAdmissionPolicyBinding) field.ErrorList { 1139 return validateValidatingAdmissionPolicyBinding(pb) 1140 } 1141 1142 func validateValidatingAdmissionPolicyBinding(pb *admissionregistration.ValidatingAdmissionPolicyBinding) field.ErrorList { 1143 allErrors := genericvalidation.ValidateObjectMeta(&pb.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) 1144 allErrors = append(allErrors, validateValidatingAdmissionPolicyBindingSpec(&pb.Spec, field.NewPath("spec"))...) 1145 1146 return allErrors 1147 } 1148 1149 func validateValidatingAdmissionPolicyBindingSpec(spec *admissionregistration.ValidatingAdmissionPolicyBindingSpec, fldPath *field.Path) field.ErrorList { 1150 var allErrors field.ErrorList 1151 1152 if len(spec.PolicyName) == 0 { 1153 allErrors = append(allErrors, field.Required(fldPath.Child("policyName"), "")) 1154 } else { 1155 for _, msg := range genericvalidation.NameIsDNSSubdomain(spec.PolicyName, false) { 1156 allErrors = append(allErrors, field.Invalid(fldPath.Child("policyName"), spec.PolicyName, msg)) 1157 } 1158 } 1159 allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...) 1160 allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResouces"))...) 1161 allErrors = append(allErrors, validateValidationActions(spec.ValidationActions, fldPath.Child("validationActions"))...) 1162 1163 return allErrors 1164 } 1165 1166 func validateParamRef(pr *admissionregistration.ParamRef, fldPath *field.Path) field.ErrorList { 1167 var allErrors field.ErrorList 1168 if pr == nil { 1169 return allErrors 1170 } 1171 1172 if len(pr.Name) > 0 { 1173 for _, msg := range path.ValidatePathSegmentName(pr.Name, false) { 1174 allErrors = append(allErrors, field.Invalid(fldPath.Child("name"), pr.Name, msg)) 1175 } 1176 1177 if pr.Selector != nil { 1178 allErrors = append(allErrors, field.Forbidden(fldPath.Child("name"), `name and selector are mutually exclusive`)) 1179 } 1180 } 1181 1182 if pr.Selector != nil { 1183 labelSelectorValidationOpts := metav1validation.LabelSelectorValidationOptions{} 1184 allErrors = append(allErrors, metav1validation.ValidateLabelSelector(pr.Selector, labelSelectorValidationOpts, fldPath.Child("selector"))...) 1185 1186 if len(pr.Name) > 0 { 1187 allErrors = append(allErrors, field.Forbidden(fldPath.Child("selector"), `name and selector are mutually exclusive`)) 1188 } 1189 } 1190 1191 if len(pr.Name) == 0 && pr.Selector == nil { 1192 allErrors = append(allErrors, field.Required(fldPath, `one of name or selector must be specified`)) 1193 } 1194 1195 if pr.ParameterNotFoundAction == nil || len(*pr.ParameterNotFoundAction) == 0 { 1196 allErrors = append(allErrors, field.Required(fldPath.Child("parameterNotFoundAction"), "")) 1197 } else { 1198 if *pr.ParameterNotFoundAction != admissionregistration.DenyAction && *pr.ParameterNotFoundAction != admissionregistration.AllowAction { 1199 allErrors = append(allErrors, field.NotSupported(fldPath.Child("parameterNotFoundAction"), pr.ParameterNotFoundAction, []string{string(admissionregistration.DenyAction), string(admissionregistration.AllowAction)})) 1200 } 1201 } 1202 1203 return allErrors 1204 } 1205 1206 // ValidateValidatingAdmissionPolicyUpdate validates update of validating admission policy 1207 func ValidateValidatingAdmissionPolicyUpdate(newC, oldC *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList { 1208 return validateValidatingAdmissionPolicy(newC, validationOptions{ 1209 ignoreMatchConditions: ignoreValidatingAdmissionPolicyMatchConditions(newC, oldC), 1210 preexistingExpressions: findValidatingPolicyPreexistingExpressions(oldC), 1211 strictCostEnforcement: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP), 1212 }) 1213 } 1214 1215 // ValidateValidatingAdmissionPolicyStatusUpdate validates update of status of validating admission policy 1216 func ValidateValidatingAdmissionPolicyStatusUpdate(newC, oldC *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList { 1217 return validateValidatingAdmissionPolicyStatus(&newC.Status, field.NewPath("status")) 1218 } 1219 1220 // ValidateValidatingAdmissionPolicyBindingUpdate validates update of validating admission policy 1221 func ValidateValidatingAdmissionPolicyBindingUpdate(newC, oldC *admissionregistration.ValidatingAdmissionPolicyBinding) field.ErrorList { 1222 return validateValidatingAdmissionPolicyBinding(newC) 1223 } 1224 1225 func validateValidatingAdmissionPolicyStatus(status *admissionregistration.ValidatingAdmissionPolicyStatus, fldPath *field.Path) field.ErrorList { 1226 var allErrors field.ErrorList 1227 allErrors = append(allErrors, validateTypeChecking(status.TypeChecking, fldPath.Child("typeChecking"))...) 1228 allErrors = append(allErrors, metav1validation.ValidateConditions(status.Conditions, fldPath.Child("conditions"))...) 1229 return allErrors 1230 } 1231 1232 func validateTypeChecking(typeChecking *admissionregistration.TypeChecking, fldPath *field.Path) field.ErrorList { 1233 if typeChecking == nil { 1234 return nil 1235 } 1236 return validateExpressionWarnings(typeChecking.ExpressionWarnings, fldPath.Child("expressionWarnings")) 1237 } 1238 1239 func validateExpressionWarnings(expressionWarnings []admissionregistration.ExpressionWarning, fldPath *field.Path) field.ErrorList { 1240 var allErrors field.ErrorList 1241 for i, warning := range expressionWarnings { 1242 allErrors = append(allErrors, validateExpressionWarning(&warning, fldPath.Index(i))...) 1243 } 1244 return allErrors 1245 } 1246 1247 func validateExpressionWarning(expressionWarning *admissionregistration.ExpressionWarning, fldPath *field.Path) field.ErrorList { 1248 var allErrors field.ErrorList 1249 if expressionWarning.Warning == "" { 1250 allErrors = append(allErrors, field.Required(fldPath.Child("warning"), "")) 1251 } 1252 allErrors = append(allErrors, validateFieldRef(expressionWarning.FieldRef, fldPath.Child("fieldRef"))...) 1253 return allErrors 1254 } 1255 1256 func validateFieldRef(fieldRef string, fldPath *field.Path) field.ErrorList { 1257 fieldRef = strings.TrimSpace(fieldRef) 1258 if fieldRef == "" { 1259 return field.ErrorList{field.Required(fldPath, "")} 1260 } 1261 jsonPath := jsonpath.New("spec") 1262 if err := jsonPath.Parse(fmt.Sprintf("{%s}", fieldRef)); err != nil { 1263 return field.ErrorList{field.Invalid(fldPath, fieldRef, fmt.Sprintf("invalid JSONPath: %v", err))} 1264 } 1265 // no further checks, for an easier upgrade/rollback 1266 return nil 1267 } 1268 1269 // statelessCELCompiler does not support variable composition (and thus is stateless). It should be used when 1270 // variable composition is not allowed, for example, when validating MatchConditions. 1271 // strictStatelessCELCompiler is a cel Compiler that enforces strict cost enforcement. 1272 // nonStrictStatelessCELCompiler is a cel Compiler that does not enforce strict cost enforcement. 1273 var strictStatelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) 1274 var nonStrictStatelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), false)) 1275 1276 func createCompiler(allowComposition, strictCost bool) plugincel.Compiler { 1277 if !allowComposition { 1278 if strictCost { 1279 return strictStatelessCELCompiler 1280 } else { 1281 return nonStrictStatelessCELCompiler 1282 } 1283 } 1284 compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), strictCost)) 1285 if err != nil { 1286 // should never happen, but cannot panic either. 1287 utilruntime.HandleError(err) 1288 return plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), strictCost)) 1289 } 1290 return compiler 1291 } 1292 1293 var celIdentRegex = regexp.MustCompile("^[_a-zA-Z][_a-zA-Z0-9]*$") 1294 var celReserved = sets.NewString("true", "false", "null", "in", 1295 "as", "break", "const", "continue", "else", 1296 "for", "function", "if", "import", "let", 1297 "loop", "package", "namespace", "return", 1298 "var", "void", "while") 1299 1300 func isCELIdentifier(name string) bool { 1301 // IDENT ::= [_a-zA-Z][_a-zA-Z0-9]* - RESERVED 1302 // BOOL_LIT ::= "true" | "false" 1303 // NULL_LIT ::= "null" 1304 // RESERVED ::= BOOL_LIT | NULL_LIT | "in" 1305 // | "as" | "break" | "const" | "continue" | "else" 1306 // | "for" | "function" | "if" | "import" | "let" 1307 // | "loop" | "package" | "namespace" | "return" 1308 // | "var" | "void" | "while" 1309 return celIdentRegex.MatchString(name) && !celReserved.Has(name) 1310 }