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