Merge pull request #124675 from cici37/fgForCost
Adding a deprecating featurer gate to fix cost
This commit is contained in:
		| @@ -35,6 +35,8 @@ import ( | |||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" | 	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" | ||||||
| 	"k8s.io/apiserver/pkg/cel" | 	"k8s.io/apiserver/pkg/cel" | ||||||
| 	"k8s.io/apiserver/pkg/cel/environment" | 	"k8s.io/apiserver/pkg/cel/environment" | ||||||
|  | 	"k8s.io/apiserver/pkg/features" | ||||||
|  | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
| 	"k8s.io/apiserver/pkg/util/webhook" | 	"k8s.io/apiserver/pkg/util/webhook" | ||||||
| 	"k8s.io/client-go/util/jsonpath" | 	"k8s.io/client-go/util/jsonpath" | ||||||
|  |  | ||||||
| @@ -221,6 +223,7 @@ func ValidateValidatingWebhookConfiguration(e *admissionregistration.ValidatingW | |||||||
| 		requireRecognizedAdmissionReviewVersion: true, | 		requireRecognizedAdmissionReviewVersion: true, | ||||||
| 		requireUniqueWebhookNames:               true, | 		requireUniqueWebhookNames:               true, | ||||||
| 		allowInvalidLabelValueInSelector:        false, | 		allowInvalidLabelValueInSelector:        false, | ||||||
|  | 		strictCostEnforcement:                   utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -250,6 +253,7 @@ func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebho | |||||||
| 		requireRecognizedAdmissionReviewVersion: true, | 		requireRecognizedAdmissionReviewVersion: true, | ||||||
| 		requireUniqueWebhookNames:               true, | 		requireUniqueWebhookNames:               true, | ||||||
| 		allowInvalidLabelValueInSelector:        false, | 		allowInvalidLabelValueInSelector:        false, | ||||||
|  | 		strictCostEnforcement:                   utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -261,6 +265,7 @@ type validationOptions struct { | |||||||
| 	requireUniqueWebhookNames               bool | 	requireUniqueWebhookNames               bool | ||||||
| 	allowInvalidLabelValueInSelector        bool | 	allowInvalidLabelValueInSelector        bool | ||||||
| 	preexistingExpressions                  preexistingExpressions | 	preexistingExpressions                  preexistingExpressions | ||||||
|  | 	strictCostEnforcement                   bool | ||||||
| } | } | ||||||
|  |  | ||||||
| type preexistingExpressions struct { | type preexistingExpressions struct { | ||||||
| @@ -687,6 +692,7 @@ func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistrat | |||||||
| 		requireUniqueWebhookNames:               validatingHasUniqueWebhookNames(oldC.Webhooks), | 		requireUniqueWebhookNames:               validatingHasUniqueWebhookNames(oldC.Webhooks), | ||||||
| 		allowInvalidLabelValueInSelector:        validatingWebhookHasInvalidLabelValueInSelector(oldC.Webhooks), | 		allowInvalidLabelValueInSelector:        validatingWebhookHasInvalidLabelValueInSelector(oldC.Webhooks), | ||||||
| 		preexistingExpressions:                  findValidatingPreexistingExpressions(oldC), | 		preexistingExpressions:                  findValidatingPreexistingExpressions(oldC), | ||||||
|  | 		strictCostEnforcement:                   utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -700,6 +706,7 @@ func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistratio | |||||||
| 		requireUniqueWebhookNames:               mutatingHasUniqueWebhookNames(oldC.Webhooks), | 		requireUniqueWebhookNames:               mutatingHasUniqueWebhookNames(oldC.Webhooks), | ||||||
| 		allowInvalidLabelValueInSelector:        mutatingWebhookHasInvalidLabelValueInSelector(oldC.Webhooks), | 		allowInvalidLabelValueInSelector:        mutatingWebhookHasInvalidLabelValueInSelector(oldC.Webhooks), | ||||||
| 		preexistingExpressions:                  findMutatingPreexistingExpressions(oldC), | 		preexistingExpressions:                  findMutatingPreexistingExpressions(oldC), | ||||||
|  | 		strictCostEnforcement:                   utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -713,7 +720,7 @@ const ( | |||||||
|  |  | ||||||
| // ValidateValidatingAdmissionPolicy validates a ValidatingAdmissionPolicy before creation. | // ValidateValidatingAdmissionPolicy validates a ValidatingAdmissionPolicy before creation. | ||||||
| func ValidateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList { | func ValidateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList { | ||||||
| 	return validateValidatingAdmissionPolicy(p, validationOptions{ignoreMatchConditions: false}) | 	return validateValidatingAdmissionPolicy(p, validationOptions{ignoreMatchConditions: false, strictCostEnforcement: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP)}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func validateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy, opts validationOptions) field.ErrorList { | func validateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy, opts validationOptions) field.ErrorList { | ||||||
| @@ -728,7 +735,7 @@ func validateValidatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissi | |||||||
| 	getCompiler := func() plugincel.Compiler { | 	getCompiler := func() plugincel.Compiler { | ||||||
| 		if compiler == nil { | 		if compiler == nil { | ||||||
| 			needsComposition := len(spec.Variables) > 0 | 			needsComposition := len(spec.Variables) > 0 | ||||||
| 			compiler = createCompiler(needsComposition) | 			compiler = createCompiler(needsComposition, opts.strictCostEnforcement) | ||||||
| 		} | 		} | ||||||
| 		return compiler | 		return compiler | ||||||
| 	} | 	} | ||||||
| @@ -973,6 +980,7 @@ func validateVariable(compiler plugincel.Compiler, v *admissionregistration.Vari | |||||||
| 			result := compiler.CompileAndStoreVariable(variable, plugincel.OptionalVariableDeclarations{ | 			result := compiler.CompileAndStoreVariable(variable, plugincel.OptionalVariableDeclarations{ | ||||||
| 				HasParams:     paramKind != nil, | 				HasParams:     paramKind != nil, | ||||||
| 				HasAuthorizer: true, | 				HasAuthorizer: true, | ||||||
|  | 				StrictCost:    opts.strictCostEnforcement, | ||||||
| 			}, envType) | 			}, envType) | ||||||
| 			if result.Error != nil { | 			if result.Error != nil { | ||||||
| 				allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), variable, result.Error)) | 				allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), variable, result.Error)) | ||||||
| @@ -1047,6 +1055,7 @@ func validateValidationExpression(compiler plugincel.Compiler, expression string | |||||||
| 	}, plugincel.OptionalVariableDeclarations{ | 	}, plugincel.OptionalVariableDeclarations{ | ||||||
| 		HasParams:     hasParams, | 		HasParams:     hasParams, | ||||||
| 		HasAuthorizer: true, | 		HasAuthorizer: true, | ||||||
|  | 		StrictCost:    opts.strictCostEnforcement, | ||||||
| 	}, envType, fldPath) | 	}, envType, fldPath) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -1055,11 +1064,18 @@ func validateMatchConditionsExpression(expression string, opts validationOptions | |||||||
| 	if opts.preexistingExpressions.matchConditionExpressions.Has(expression) { | 	if opts.preexistingExpressions.matchConditionExpressions.Has(expression) { | ||||||
| 		envType = environment.StoredExpressions | 		envType = environment.StoredExpressions | ||||||
| 	} | 	} | ||||||
| 	return validateCELCondition(statelessCELCompiler, &matchconditions.MatchCondition{ | 	var compiler plugincel.Compiler | ||||||
|  | 	if opts.strictCostEnforcement { | ||||||
|  | 		compiler = strictStatelessCELCompiler | ||||||
|  | 	} else { | ||||||
|  | 		compiler = nonStrictStatelessCELCompiler | ||||||
|  | 	} | ||||||
|  | 	return validateCELCondition(compiler, &matchconditions.MatchCondition{ | ||||||
| 		Expression: expression, | 		Expression: expression, | ||||||
| 	}, plugincel.OptionalVariableDeclarations{ | 	}, plugincel.OptionalVariableDeclarations{ | ||||||
| 		HasParams:     opts.allowParamsInMatchConditions, | 		HasParams:     opts.allowParamsInMatchConditions, | ||||||
| 		HasAuthorizer: true, | 		HasAuthorizer: true, | ||||||
|  | 		StrictCost:    opts.strictCostEnforcement, | ||||||
| 	}, envType, fldPath) | 	}, envType, fldPath) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -1073,6 +1089,7 @@ func validateMessageExpression(compiler plugincel.Compiler, expression string, o | |||||||
| 	}, plugincel.OptionalVariableDeclarations{ | 	}, plugincel.OptionalVariableDeclarations{ | ||||||
| 		HasParams:     opts.allowParamsInMatchConditions, | 		HasParams:     opts.allowParamsInMatchConditions, | ||||||
| 		HasAuthorizer: false, | 		HasAuthorizer: false, | ||||||
|  | 		StrictCost:    opts.strictCostEnforcement, | ||||||
| 	}, envType, fldPath) | 	}, envType, fldPath) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -1097,7 +1114,7 @@ func validateAuditAnnotation(compiler plugincel.Compiler, meta metav1.ObjectMeta | |||||||
| 		} | 		} | ||||||
| 		result := compiler.CompileCELExpression(&validatingadmissionpolicy.AuditAnnotationCondition{ | 		result := compiler.CompileCELExpression(&validatingadmissionpolicy.AuditAnnotationCondition{ | ||||||
| 			ValueExpression: trimmedValueExpression, | 			ValueExpression: trimmedValueExpression, | ||||||
| 		}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}, envType) | 		}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: opts.strictCostEnforcement}, envType) | ||||||
| 		if result.Error != nil { | 		if result.Error != nil { | ||||||
| 			switch result.Error.Type { | 			switch result.Error.Type { | ||||||
| 			case cel.ErrorTypeRequired: | 			case cel.ErrorTypeRequired: | ||||||
| @@ -1191,6 +1208,7 @@ func ValidateValidatingAdmissionPolicyUpdate(newC, oldC *admissionregistration.V | |||||||
| 	return validateValidatingAdmissionPolicy(newC, validationOptions{ | 	return validateValidatingAdmissionPolicy(newC, validationOptions{ | ||||||
| 		ignoreMatchConditions:  ignoreValidatingAdmissionPolicyMatchConditions(newC, oldC), | 		ignoreMatchConditions:  ignoreValidatingAdmissionPolicyMatchConditions(newC, oldC), | ||||||
| 		preexistingExpressions: findValidatingPolicyPreexistingExpressions(oldC), | 		preexistingExpressions: findValidatingPolicyPreexistingExpressions(oldC), | ||||||
|  | 		strictCostEnforcement:  utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -1250,17 +1268,24 @@ func validateFieldRef(fieldRef string, fldPath *field.Path) field.ErrorList { | |||||||
|  |  | ||||||
| // statelessCELCompiler does not support variable composition (and thus is stateless). It should be used when | // statelessCELCompiler does not support variable composition (and thus is stateless). It should be used when | ||||||
| // variable composition is not allowed, for example, when validating MatchConditions. | // variable composition is not allowed, for example, when validating MatchConditions. | ||||||
| var statelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | // strictStatelessCELCompiler is a cel Compiler that enforces strict cost enforcement. | ||||||
|  | // nonStrictStatelessCELCompiler is a cel Compiler that does not enforce strict cost enforcement. | ||||||
|  | var strictStatelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
|  | var nonStrictStatelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), false)) | ||||||
|  |  | ||||||
| func createCompiler(allowComposition bool) plugincel.Compiler { | func createCompiler(allowComposition, strictCost bool) plugincel.Compiler { | ||||||
| 	if !allowComposition { | 	if !allowComposition { | ||||||
| 		return statelessCELCompiler | 		if strictCost { | ||||||
|  | 			return strictStatelessCELCompiler | ||||||
|  | 		} else { | ||||||
|  | 			return nonStrictStatelessCELCompiler | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), strictCost)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		// should never happen, but cannot panic either. | 		// should never happen, but cannot panic either. | ||||||
| 		utilruntime.HandleError(err) | 		utilruntime.HandleError(err) | ||||||
| 		return plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 		return plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), strictCost)) | ||||||
| 	} | 	} | ||||||
| 	return compiler | 	return compiler | ||||||
| } | } | ||||||
|   | |||||||
| @@ -29,6 +29,8 @@ import ( | |||||||
| 	plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" | 	plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" | ||||||
| 	"k8s.io/apiserver/pkg/cel/environment" | 	"k8s.io/apiserver/pkg/cel/environment" | ||||||
| 	"k8s.io/apiserver/pkg/cel/library" | 	"k8s.io/apiserver/pkg/cel/library" | ||||||
|  | 	"k8s.io/apiserver/pkg/features" | ||||||
|  | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/admissionregistration" | 	"k8s.io/kubernetes/pkg/apis/admissionregistration" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -3412,8 +3414,9 @@ func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		// TODO: CustomAuditAnnotations: string valueExpression with {oldObject} is allowed | 		// TODO: CustomAuditAnnotations: string valueExpression with {oldObject} is allowed | ||||||
| 	} | 	} | ||||||
|  | 	strictCost := utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP) | ||||||
| 	// Include the test library, which includes the test() function in the storage environment during test | 	// Include the test library, which includes the test() function in the storage environment during test | ||||||
| 	base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) | 	base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), strictCost) | ||||||
| 	extended, err := base.Extend(environment.VersionedOptions{ | 	extended, err := base.Extend(environment.VersionedOptions{ | ||||||
| 		IntroducedVersion: version.MustParseGeneric("1.999"), | 		IntroducedVersion: version.MustParseGeneric("1.999"), | ||||||
| 		EnvOptions:        []cel.EnvOption{library.Test()}, | 		EnvOptions:        []cel.EnvOption{library.Test()}, | ||||||
| @@ -3421,10 +3424,17 @@ func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	statelessCELCompiler = plugincel.NewCompiler(extended) | 	if strictCost { | ||||||
| 	defer func() { | 		strictStatelessCELCompiler = plugincel.NewCompiler(extended) | ||||||
| 		statelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 		defer func() { | ||||||
| 	}() | 			strictStatelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), strictCost)) | ||||||
|  | 		}() | ||||||
|  | 	} else { | ||||||
|  | 		nonStrictStatelessCELCompiler = plugincel.NewCompiler(extended) | ||||||
|  | 		defer func() { | ||||||
|  | 			nonStrictStatelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), strictCost)) | ||||||
|  | 		}() | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for _, test := range tests { | 	for _, test := range tests { | ||||||
| 		t.Run(test.name, func(t *testing.T) { | 		t.Run(test.name, func(t *testing.T) { | ||||||
|   | |||||||
| @@ -1234,6 +1234,10 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS | |||||||
|  |  | ||||||
| 	genericfeatures.StorageVersionHash: {Default: true, PreRelease: featuregate.Beta}, | 	genericfeatures.StorageVersionHash: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
|  | 	genericfeatures.StrictCostEnforcementForVAP: {Default: false, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
|  | 	genericfeatures.StrictCostEnforcementForWebhooks: {Default: false, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
| 	genericfeatures.StructuredAuthenticationConfiguration: {Default: true, PreRelease: featuregate.Beta}, | 	genericfeatures.StructuredAuthenticationConfiguration: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
| 	genericfeatures.StructuredAuthorizationConfiguration: {Default: true, PreRelease: featuregate.Beta}, | 	genericfeatures.StructuredAuthorizationConfiguration: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|   | |||||||
| @@ -92,7 +92,8 @@ func ValidateCustomResourceDefinition(ctx context.Context, obj *apiextensions.Cu | |||||||
| 		requirePrunedDefaults:                    true, | 		requirePrunedDefaults:                    true, | ||||||
| 		requireAtomicSetType:                     true, | 		requireAtomicSetType:                     true, | ||||||
| 		requireMapListKeysMapSetValidation:       true, | 		requireMapListKeysMapSetValidation:       true, | ||||||
| 		celEnvironmentSet:                        environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()), | 		// strictCost is always true to enforce cost limits. | ||||||
|  | 		celEnvironmentSet: environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata")) | 	allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata")) | ||||||
| @@ -231,7 +232,8 @@ func ValidateCustomResourceDefinitionUpdate(ctx context.Context, obj, oldObj *ap | |||||||
| 		requireMapListKeysMapSetValidation:       requireMapListKeysMapSetValidation(&oldObj.Spec), | 		requireMapListKeysMapSetValidation:       requireMapListKeysMapSetValidation(&oldObj.Spec), | ||||||
| 		preexistingExpressions:                   findPreexistingExpressions(&oldObj.Spec), | 		preexistingExpressions:                   findPreexistingExpressions(&oldObj.Spec), | ||||||
| 		versionsWithUnchangedSchemas:             findVersionsWithUnchangedSchemas(obj, oldObj), | 		versionsWithUnchangedSchemas:             findVersionsWithUnchangedSchemas(obj, oldObj), | ||||||
| 		celEnvironmentSet:                        environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()), | 		// strictCost is always true to enforce cost limits. | ||||||
|  | 		celEnvironmentSet: environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true), | ||||||
| 	} | 	} | ||||||
| 	return validateCustomResourceDefinitionUpdate(ctx, obj, oldObj, opts) | 	return validateCustomResourceDefinitionUpdate(ctx, obj, oldObj, opts) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7312,7 +7312,7 @@ func TestValidateCustomResourceDefinitionValidationRuleCompatibility(t *testing. | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Include the test library, which includes the test() function in the storage environment during test | 	// Include the test library, which includes the test() function in the storage environment during test | ||||||
| 	base := environment.MustBaseEnvSet(version.MajorMinor(1, 998)) | 	base := environment.MustBaseEnvSet(version.MajorMinor(1, 998), true) | ||||||
| 	envSet, err := base.Extend(environment.VersionedOptions{ | 	envSet, err := base.Extend(environment.VersionedOptions{ | ||||||
| 		IntroducedVersion: version.MajorMinor(1, 999), | 		IntroducedVersion: version.MajorMinor(1, 999), | ||||||
| 		EnvOptions:        []cel.EnvOption{library.Test()}, | 		EnvOptions:        []cel.EnvOption{library.Test()}, | ||||||
| @@ -10104,7 +10104,7 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) { | |||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
| 			ctx := context.TODO() | 			ctx := context.TODO() | ||||||
| 			if tt.opts.celEnvironmentSet == nil { | 			if tt.opts.celEnvironmentSet == nil { | ||||||
| 				tt.opts.celEnvironmentSet = environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) | 				tt.opts.celEnvironmentSet = environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true) | ||||||
| 			} | 			} | ||||||
| 			got := validateCustomResourceDefinitionValidation(ctx, &tt.input, tt.statusEnabled, tt.opts, field.NewPath("spec", "validation")) | 			got := validateCustomResourceDefinitionValidation(ctx, &tt.input, tt.statusEnabled, tt.opts, field.NewPath("spec", "validation")) | ||||||
|  |  | ||||||
| @@ -10635,7 +10635,7 @@ func TestCelContext(t *testing.T) { | |||||||
| 			celContext := RootCELContext(tt.schema) | 			celContext := RootCELContext(tt.schema) | ||||||
| 			celContext.converter = converter | 			celContext.converter = converter | ||||||
| 			opts := validationOptions{ | 			opts := validationOptions{ | ||||||
| 				celEnvironmentSet: environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()), | 				celEnvironmentSet: environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true), | ||||||
| 			} | 			} | ||||||
| 			openAPIV3Schema := &specStandardValidatorV3{ | 			openAPIV3Schema := &specStandardValidatorV3{ | ||||||
| 				allowDefaults:            opts.allowDefaults, | 				allowDefaults:            opts.allowDefaults, | ||||||
|   | |||||||
| @@ -850,7 +850,7 @@ func TestCelCompilation(t *testing.T) { | |||||||
|  |  | ||||||
| 	for _, tt := range cases { | 	for _, tt := range cases { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
| 			env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend( | 			env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true).Extend( | ||||||
| 				environment.VersionedOptions{ | 				environment.VersionedOptions{ | ||||||
| 					IntroducedVersion: version.MajorMinor(1, 999), | 					IntroducedVersion: version.MajorMinor(1, 999), | ||||||
| 					EnvOptions:        []celgo.EnvOption{celgo.Lib(&fakeLib{})}, | 					EnvOptions:        []celgo.EnvOption{celgo.Lib(&fakeLib{})}, | ||||||
| @@ -1327,7 +1327,7 @@ func genMapWithCustomItemRule(item *schema.Structural, rule string) func(maxProp | |||||||
| // if expectedCostExceedsLimit is non-zero. Typically, only expectedCost or expectedCostExceedsLimit is non-zero, not both. | // if expectedCostExceedsLimit is non-zero. Typically, only expectedCost or expectedCostExceedsLimit is non-zero, not both. | ||||||
| func schemaChecker(schema *schema.Structural, expectedCost uint64, expectedCostExceedsLimit uint64, t *testing.T) func(t *testing.T) { | func schemaChecker(schema *schema.Structural, expectedCost uint64, expectedCostExceedsLimit uint64, t *testing.T) func(t *testing.T) { | ||||||
| 	return func(t *testing.T) { | 	return func(t *testing.T) { | ||||||
| 		compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), celconfig.PerCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()), NewExpressionsEnvLoader()) | 		compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), celconfig.PerCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true), NewExpressionsEnvLoader()) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("Expected no error, got: %v", err) | 			t.Fatalf("Expected no error, got: %v", err) | ||||||
| 		} | 		} | ||||||
| @@ -1885,7 +1885,7 @@ func TestCostEstimation(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func BenchmarkCompile(b *testing.B) { | func BenchmarkCompile(b *testing.B) { | ||||||
| 	env := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) // prepare the environment | 	env := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true) // prepare the environment | ||||||
| 	s := genArrayWithRule("number", "true")(nil) | 	s := genArrayWithRule("number", "true")(nil) | ||||||
| 	b.ReportAllocs() | 	b.ReportAllocs() | ||||||
| 	b.ResetTimer() | 	b.ResetTimer() | ||||||
|   | |||||||
| @@ -90,7 +90,8 @@ func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64 | |||||||
| // exist. declType is expected to be a CEL DeclType corresponding to the structural schema. | // exist. declType is expected to be a CEL DeclType corresponding to the structural schema. | ||||||
| // perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input. | // perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input. | ||||||
| func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType, perCallLimit uint64) *Validator { | func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType, perCallLimit uint64) *Validator { | ||||||
| 	compiledRules, err := Compile(s, declType, perCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()), StoredExpressionsEnvLoader()) | 	// strictCost is always true to enforce cost limits. | ||||||
|  | 	compiledRules, err := Compile(s, declType, perCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true), StoredExpressionsEnvLoader()) | ||||||
| 	var itemsValidator, additionalPropertiesValidator *Validator | 	var itemsValidator, additionalPropertiesValidator *Validator | ||||||
| 	var propertiesValidators map[string]Validator | 	var propertiesValidators map[string]Validator | ||||||
| 	if s.Items != nil { | 	if s.Items != nil { | ||||||
|   | |||||||
| @@ -61,6 +61,7 @@ func TestValidationExpressions(t *testing.T) { | |||||||
| 		costBudget    int64 | 		costBudget    int64 | ||||||
| 		isRoot        bool | 		isRoot        bool | ||||||
| 		expectSkipped bool | 		expectSkipped bool | ||||||
|  | 		expectedCost  int64 | ||||||
| 	}{ | 	}{ | ||||||
| 		// tests where val1 and val2 are equal but val3 is different | 		// tests where val1 and val2 are equal but val3 is different | ||||||
| 		// equality, comparisons and type specific functions | 		// equality, comparisons and type specific functions | ||||||
| @@ -2006,6 +2007,38 @@ func TestValidationExpressions(t *testing.T) { | |||||||
| 				`quantity(self.val1).isInteger()`, | 				`quantity(self.val1).isInteger()`, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{name: "cost for extended lib calculated correctly: isSorted", | ||||||
|  | 			obj:    objs("20", "200M"), | ||||||
|  | 			schema: schemas(stringType, stringType), | ||||||
|  | 			valid: []string{ | ||||||
|  | 				"[1,2,3,4].isSorted()", | ||||||
|  | 			}, | ||||||
|  | 			expectedCost: 4, | ||||||
|  | 		}, | ||||||
|  | 		{name: "cost for extended lib calculated correctly: url", | ||||||
|  | 			obj:    objs("20", "200M"), | ||||||
|  | 			schema: schemas(stringType, stringType), | ||||||
|  | 			valid: []string{ | ||||||
|  | 				"url('https:://kubernetes.io/').getHostname() != 'test'", | ||||||
|  | 			}, | ||||||
|  | 			expectedCost: 4, | ||||||
|  | 		}, | ||||||
|  | 		{name: "cost for extended lib calculated correctly: split", | ||||||
|  | 			obj:    objs("20", "200M"), | ||||||
|  | 			schema: schemas(stringType, stringType), | ||||||
|  | 			valid: []string{ | ||||||
|  | 				"size('abc 123 def 123'.split(' ')) > 0", | ||||||
|  | 			}, | ||||||
|  | 			expectedCost: 5, | ||||||
|  | 		}, | ||||||
|  | 		{name: "cost for extended lib calculated correctly: join", | ||||||
|  | 			obj:    objs("20", "200M"), | ||||||
|  | 			schema: schemas(stringType, stringType), | ||||||
|  | 			valid: []string{ | ||||||
|  | 				"size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0", | ||||||
|  | 			}, | ||||||
|  | 			expectedCost: 7, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i := range tests { | 	for i := range tests { | ||||||
| @@ -2013,7 +2046,9 @@ func TestValidationExpressions(t *testing.T) { | |||||||
| 		t.Run(tests[i].name, func(t *testing.T) { | 		t.Run(tests[i].name, func(t *testing.T) { | ||||||
| 			t.Parallel() | 			t.Parallel() | ||||||
| 			tt := tests[i] | 			tt := tests[i] | ||||||
| 			tt.costBudget = celconfig.RuntimeCELCostBudget | 			if tt.costBudget == 0 { | ||||||
|  | 				tt.costBudget = celconfig.RuntimeCELCostBudget | ||||||
|  | 			} | ||||||
| 			ctx := context.TODO() | 			ctx := context.TODO() | ||||||
| 			for j := range tt.valid { | 			for j := range tt.valid { | ||||||
| 				validRule := tt.valid[j] | 				validRule := tt.valid[j] | ||||||
| @@ -2032,6 +2067,13 @@ func TestValidationExpressions(t *testing.T) { | |||||||
| 					for _, err := range errs { | 					for _, err := range errs { | ||||||
| 						t.Errorf("unexpected error: %v", err) | 						t.Errorf("unexpected error: %v", err) | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
|  | 					if tt.expectedCost != 0 { | ||||||
|  | 						if remainingBudget != tt.costBudget-tt.expectedCost { | ||||||
|  | 							t.Errorf("expected cost to be %d, but got %d", tt.expectedCost, tt.costBudget-remainingBudget) | ||||||
|  | 						} | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
| 					if tt.expectSkipped { | 					if tt.expectSkipped { | ||||||
| 						// Skipped validations should have no cost. The only possible false positive here would be the CEL expression 'true'. | 						// Skipped validations should have no cost. The only possible false positive here would be the CEL expression 'true'. | ||||||
| 						if remainingBudget != tt.costBudget { | 						if remainingBudget != tt.costBudget { | ||||||
|   | |||||||
| @@ -222,40 +222,48 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op | |||||||
| func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs { | func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs { | ||||||
| 	requestType := BuildRequestType() | 	requestType := BuildRequestType() | ||||||
| 	namespaceType := BuildNamespaceType() | 	namespaceType := BuildNamespaceType() | ||||||
| 	envs := make(variableDeclEnvs, 4) // since the number of variable combinations is small, pre-build a environment for each | 	envs := make(variableDeclEnvs, 8) // since the number of variable combinations is small, pre-build a environment for each | ||||||
| 	for _, hasParams := range []bool{false, true} { | 	for _, hasParams := range []bool{false, true} { | ||||||
| 		for _, hasAuthorizer := range []bool{false, true} { | 		for _, hasAuthorizer := range []bool{false, true} { | ||||||
| 			var envOpts []cel.EnvOption | 			for _, strictCost := range []bool{false, true} { | ||||||
| 			if hasParams { | 				var envOpts []cel.EnvOption | ||||||
| 				envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType)) | 				if hasParams { | ||||||
| 			} | 					envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType)) | ||||||
| 			if hasAuthorizer { | 				} | ||||||
|  | 				if hasAuthorizer { | ||||||
|  | 					envOpts = append(envOpts, | ||||||
|  | 						cel.Variable(AuthorizerVarName, library.AuthorizerType), | ||||||
|  | 						cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType)) | ||||||
|  | 				} | ||||||
| 				envOpts = append(envOpts, | 				envOpts = append(envOpts, | ||||||
| 					cel.Variable(AuthorizerVarName, library.AuthorizerType), | 					cel.Variable(ObjectVarName, cel.DynType), | ||||||
| 					cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType)) | 					cel.Variable(OldObjectVarName, cel.DynType), | ||||||
| 			} | 					cel.Variable(NamespaceVarName, namespaceType.CelType()), | ||||||
| 			envOpts = append(envOpts, | 					cel.Variable(RequestVarName, requestType.CelType())) | ||||||
| 				cel.Variable(ObjectVarName, cel.DynType), |  | ||||||
| 				cel.Variable(OldObjectVarName, cel.DynType), |  | ||||||
| 				cel.Variable(NamespaceVarName, namespaceType.CelType()), |  | ||||||
| 				cel.Variable(RequestVarName, requestType.CelType())) |  | ||||||
|  |  | ||||||
| 			extended, err := baseEnv.Extend( | 				extended, err := baseEnv.Extend( | ||||||
| 				environment.VersionedOptions{ | 					environment.VersionedOptions{ | ||||||
| 					// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these | 						// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these | ||||||
| 					// options should always be present. | 						// options should always be present. | ||||||
| 					IntroducedVersion: version.MajorMinor(1, 0), | 						IntroducedVersion: version.MajorMinor(1, 0), | ||||||
| 					EnvOptions:        envOpts, | 						EnvOptions:        envOpts, | ||||||
| 					DeclTypes: []*apiservercel.DeclType{ | 						DeclTypes: []*apiservercel.DeclType{ | ||||||
| 						namespaceType, | 							namespaceType, | ||||||
| 						requestType, | 							requestType, | ||||||
|  | 						}, | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				) | ||||||
| 			) | 				if err != nil { | ||||||
| 			if err != nil { | 					panic(fmt.Sprintf("environment misconfigured: %v", err)) | ||||||
| 				panic(fmt.Sprintf("environment misconfigured: %v", err)) | 				} | ||||||
|  | 				if strictCost { | ||||||
|  | 					extended, err = extended.Extend(environment.StrictCostOpt) | ||||||
|  | 					if err != nil { | ||||||
|  | 						panic(fmt.Sprintf("environment misconfigured: %v", err)) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}] = extended | ||||||
| 			} | 			} | ||||||
| 			envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer}] = extended |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return envs | 	return envs | ||||||
|   | |||||||
| @@ -178,7 +178,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Include the test library, which includes the test() function in the storage environment during test | 	// Include the test library, which includes the test() function in the storage environment during test | ||||||
| 	base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) | 	base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true) | ||||||
| 	extended, err := base.Extend(environment.VersionedOptions{ | 	extended, err := base.Extend(environment.VersionedOptions{ | ||||||
| 		IntroducedVersion: version.MajorMinor(1, 999), | 		IntroducedVersion: version.MajorMinor(1, 999), | ||||||
| 		EnvOptions:        []celgo.EnvOption{library.Test()}, | 		EnvOptions:        []celgo.EnvOption{library.Test()}, | ||||||
| @@ -254,7 +254,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func BenchmarkCompile(b *testing.B) { | func BenchmarkCompile(b *testing.B) { | ||||||
| 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
| 	b.ResetTimer() | 	b.ResetTimer() | ||||||
| 	for i := 0; i < b.N; i++ { | 	for i := 0; i < b.N; i++ { | ||||||
| 		options := OptionalVariableDeclarations{HasParams: rand.Int()%2 == 0, HasAuthorizer: rand.Int()%2 == 0} | 		options := OptionalVariableDeclarations{HasParams: rand.Int()%2 == 0, HasAuthorizer: rand.Int()%2 == 0} | ||||||
|   | |||||||
| @@ -48,14 +48,15 @@ func (t *testVariable) GetName() string { | |||||||
|  |  | ||||||
| func TestCompositedPolicies(t *testing.T) { | func TestCompositedPolicies(t *testing.T) { | ||||||
| 	cases := []struct { | 	cases := []struct { | ||||||
| 		name                 string | 		name                  string | ||||||
| 		variables            []NamedExpressionAccessor | 		variables             []NamedExpressionAccessor | ||||||
| 		expression           string | 		expression            string | ||||||
| 		attributes           admission.Attributes | 		attributes            admission.Attributes | ||||||
| 		expectedResult       any | 		expectedResult        any | ||||||
| 		expectErr            bool | 		expectErr             bool | ||||||
| 		expectedErrorMessage string | 		expectedErrorMessage  string | ||||||
| 		runtimeCostBudget    int64 | 		runtimeCostBudget     int64 | ||||||
|  | 		strictCostEnforcement bool | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name: "simple", | 			name: "simple", | ||||||
| @@ -185,16 +186,45 @@ func TestCompositedPolicies(t *testing.T) { | |||||||
| 			expectErr:            true, | 			expectErr:            true, | ||||||
| 			expectedErrorMessage: "found no matching overload for '_==_' applied to '(string, int)'", | 			expectedErrorMessage: "found no matching overload for '_==_' applied to '(string, int)'", | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "with strictCostEnforcement on: exceeds cost budget", | ||||||
|  | 			variables: []NamedExpressionAccessor{ | ||||||
|  | 				&testVariable{ | ||||||
|  | 					name:       "dict", | ||||||
|  | 					expression: "'abc 123 def 123'.split(' ')", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:            endpointCreateAttributes(), | ||||||
|  | 			expression:            "size(variables.dict) > 0", | ||||||
|  | 			expectErr:             true, | ||||||
|  | 			expectedErrorMessage:  "validation failed due to running out of cost budget, no further validation rules will be run", | ||||||
|  | 			runtimeCostBudget:     5, | ||||||
|  | 			strictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "with strictCostEnforcement off: not exceed cost budget", | ||||||
|  | 			variables: []NamedExpressionAccessor{ | ||||||
|  | 				&testVariable{ | ||||||
|  | 					name:       "dict", | ||||||
|  | 					expression: "'abc 123 def 123'.split(' ')", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:            endpointCreateAttributes(), | ||||||
|  | 			expression:            "size(variables.dict) > 0", | ||||||
|  | 			expectedResult:        true, | ||||||
|  | 			runtimeCostBudget:     5, | ||||||
|  | 			strictCostEnforcement: false, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 	for _, tc := range cases { | 	for _, tc := range cases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
| 			compiler, err := NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 			compiler, err := NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.strictCostEnforcement)) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Fatal(err) | 				t.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 			compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.NewExpressions) | 			compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions) | ||||||
| 			validations := []ExpressionAccessor{&condition{Expression: tc.expression}} | 			validations := []ExpressionAccessor{&condition{Expression: tc.expression}} | ||||||
| 			f := compiler.Compile(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.NewExpressions) | 			f := compiler.Compile(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions) | ||||||
| 			versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) | 			versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Fatal(err) | 				t.Fatal(err) | ||||||
|   | |||||||
| @@ -93,8 +93,8 @@ func TestCompile(t *testing.T) { | |||||||
|  |  | ||||||
| 	for _, tc := range cases { | 	for _, tc := range cases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
| 			c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))} | 			c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))} | ||||||
| 			e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.NewExpressions) | 			e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions) | ||||||
| 			if e == nil { | 			if e == nil { | ||||||
| 				t.Fatalf("unexpected nil validator") | 				t.Fatalf("unexpected nil validator") | ||||||
| 			} | 			} | ||||||
| @@ -182,6 +182,7 @@ func TestFilter(t *testing.T) { | |||||||
| 		authorizer       authorizer.Authorizer | 		authorizer       authorizer.Authorizer | ||||||
| 		testPerCallLimit uint64 | 		testPerCallLimit uint64 | ||||||
| 		namespaceObject  *corev1.Namespace | 		namespaceObject  *corev1.Namespace | ||||||
|  | 		strictCost       bool | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name: "valid syntax for object", | 			name: "valid syntax for object", | ||||||
| @@ -762,7 +763,7 @@ func TestFilter(t *testing.T) { | |||||||
| 			if tc.testPerCallLimit == 0 { | 			if tc.testPerCallLimit == 0 { | ||||||
| 				tc.testPerCallLimit = celconfig.PerCallLimit | 				tc.testPerCallLimit = celconfig.PerCallLimit | ||||||
| 			} | 			} | ||||||
| 			env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend( | 			env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.strictCost).Extend( | ||||||
| 				environment.VersionedOptions{ | 				environment.VersionedOptions{ | ||||||
| 					IntroducedVersion: environment.DefaultCompatibilityVersion(), | 					IntroducedVersion: environment.DefaultCompatibilityVersion(), | ||||||
| 					ProgramOptions:    []celgo.ProgramOption{celgo.CostLimit(tc.testPerCallLimit)}, | 					ProgramOptions:    []celgo.ProgramOption{celgo.CostLimit(tc.testPerCallLimit)}, | ||||||
| @@ -772,7 +773,7 @@ func TestFilter(t *testing.T) { | |||||||
| 				t.Fatal(err) | 				t.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 			c := NewFilterCompiler(env) | 			c := NewFilterCompiler(env) | ||||||
| 			f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}, environment.NewExpressions) | 			f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, environment.NewExpressions) | ||||||
| 			if f == nil { | 			if f == nil { | ||||||
| 				t.Fatalf("unexpected nil validator") | 				t.Fatalf("unexpected nil validator") | ||||||
| 			} | 			} | ||||||
| @@ -815,15 +816,17 @@ func TestRuntimeCELCostBudget(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cases := []struct { | 	cases := []struct { | ||||||
| 		name                     string | 		name                        string | ||||||
| 		attributes               admission.Attributes | 		attributes                  admission.Attributes | ||||||
| 		params                   runtime.Object | 		params                      runtime.Object | ||||||
| 		validations              []ExpressionAccessor | 		validations                 []ExpressionAccessor | ||||||
| 		hasParamKind             bool | 		hasParamKind                bool | ||||||
| 		authorizer               authorizer.Authorizer | 		authorizer                  authorizer.Authorizer | ||||||
| 		testRuntimeCELCostBudget int64 | 		testRuntimeCELCostBudget    int64 | ||||||
| 		exceedBudget             bool | 		exceedBudget                bool | ||||||
| 		expectRemainingBudget    *int64 | 		expectRemainingBudget       *int64 | ||||||
|  | 		enableStrictCostEnforcement bool | ||||||
|  | 		exceedPerCallLimit          bool | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name: "expression exceed RuntimeCELCostBudget at fist expression", | 			name: "expression exceed RuntimeCELCostBudget at fist expression", | ||||||
| @@ -910,12 +913,275 @@ func TestRuntimeCELCostBudget(t *testing.T) { | |||||||
| 			testRuntimeCELCostBudget: 6, | 			testRuntimeCELCostBudget: 6, | ||||||
| 			expectRemainingBudget:    pointer.Int64(0), | 			expectRemainingBudget:    pointer.Int64(0), | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Extended library cost: authz check", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:               newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:             false, | ||||||
|  | 			exceedBudget:             false, | ||||||
|  | 			testRuntimeCELCostBudget: 6, | ||||||
|  | 			expectRemainingBudget:    pointer.Int64(0), | ||||||
|  | 			authorizer:               denyAll, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Extended library cost: isSorted()", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "[1,2,3,4].isSorted()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:               newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:             false, | ||||||
|  | 			exceedBudget:             false, | ||||||
|  | 			testRuntimeCELCostBudget: 1, | ||||||
|  | 			expectRemainingBudget:    pointer.Int64(0), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Extended library cost: url", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:               newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:             false, | ||||||
|  | 			exceedBudget:             false, | ||||||
|  | 			testRuntimeCELCostBudget: 2, | ||||||
|  | 			expectRemainingBudget:    pointer.Int64(0), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Extended library cost: split", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "size('abc 123 def 123'.split(' ')) > 0", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:               newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:             false, | ||||||
|  | 			exceedBudget:             false, | ||||||
|  | 			testRuntimeCELCostBudget: 3, | ||||||
|  | 			expectRemainingBudget:    pointer.Int64(0), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Extended library cost: join", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:               newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:             false, | ||||||
|  | 			exceedBudget:             false, | ||||||
|  | 			testRuntimeCELCostBudget: 3, | ||||||
|  | 			expectRemainingBudget:    pointer.Int64(0), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Extended library cost: find", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "size('abc 123 def 123'.find('123')) > 0", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:               newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:             false, | ||||||
|  | 			exceedBudget:             false, | ||||||
|  | 			testRuntimeCELCostBudget: 3, | ||||||
|  | 			expectRemainingBudget:    pointer.Int64(0), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Extended library cost: quantity", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:               newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:             false, | ||||||
|  | 			exceedBudget:             false, | ||||||
|  | 			testRuntimeCELCostBudget: 6, | ||||||
|  | 			expectRemainingBudget:    pointer.Int64(0), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at fist expression", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "has(object.subsets)", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    35000, | ||||||
|  | 			exceedBudget:                true, | ||||||
|  | 			authorizer:                  denyAll, | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at last expression", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    700000, | ||||||
|  | 			exceedBudget:                true, | ||||||
|  | 			authorizer:                  denyAll, | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge is not exceed", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                true, | ||||||
|  | 			params:                      configMapParams, | ||||||
|  | 			exceedBudget:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    700011, | ||||||
|  | 			expectRemainingBudget:       pointer.Int64(1), // 700011 - 700010 | ||||||
|  | 			authorizer:                  denyAll, | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge exactly covers", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                true, | ||||||
|  | 			params:                      configMapParams, | ||||||
|  | 			exceedBudget:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    700010, | ||||||
|  | 			expectRemainingBudget:       pointer.Int64(0), | ||||||
|  | 			authorizer:                  denyAll, | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: per call limit exceeds", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "!authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                true, | ||||||
|  | 			params:                      configMapParams, | ||||||
|  | 			authorizer:                  denyAll, | ||||||
|  | 			exceedPerCallLimit:          true, | ||||||
|  | 			testRuntimeCELCostBudget:    -1, | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: Extended library cost: isSorted()", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "[1,2,3,4].isSorted()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                false, | ||||||
|  | 			exceedBudget:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    4, | ||||||
|  | 			expectRemainingBudget:       pointer.Int64(0), | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: Extended library cost: url", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                false, | ||||||
|  | 			exceedBudget:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    4, | ||||||
|  | 			expectRemainingBudget:       pointer.Int64(0), | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: Extended library cost: split", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "size('abc 123 def 123'.split(' ')) > 0", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                false, | ||||||
|  | 			exceedBudget:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    5, | ||||||
|  | 			expectRemainingBudget:       pointer.Int64(0), | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: Extended library cost: join", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                false, | ||||||
|  | 			exceedBudget:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    7, | ||||||
|  | 			expectRemainingBudget:       pointer.Int64(0), | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: Extended library cost: find", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "size('abc 123 def 123'.find('123')) > 0", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                false, | ||||||
|  | 			exceedBudget:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    4, | ||||||
|  | 			expectRemainingBudget:       pointer.Int64(0), | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP enabled: Extended library cost: quantity", | ||||||
|  | 			validations: []ExpressionAccessor{ | ||||||
|  | 				&condition{ | ||||||
|  | 					Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			attributes:                  newValidAttribute(nil, false), | ||||||
|  | 			hasParamKind:                false, | ||||||
|  | 			exceedBudget:                false, | ||||||
|  | 			testRuntimeCELCostBudget:    6, | ||||||
|  | 			expectRemainingBudget:       pointer.Int64(0), | ||||||
|  | 			enableStrictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, tc := range cases { | 	for _, tc := range cases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
| 			c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))} | 			c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))} | ||||||
| 			f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: false}, environment.NewExpressions) | 			f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions) | ||||||
| 			if f == nil { | 			if f == nil { | ||||||
| 				t.Fatalf("unexpected nil validator") | 				t.Fatalf("unexpected nil validator") | ||||||
| 			} | 			} | ||||||
| @@ -928,16 +1194,28 @@ func TestRuntimeCELCostBudget(t *testing.T) { | |||||||
| 				t.Fatalf("unexpected error on conversion: %v", err) | 				t.Fatalf("unexpected error on conversion: %v", err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if tc.testRuntimeCELCostBudget == 0 { | 			if tc.testRuntimeCELCostBudget < 0 { | ||||||
| 				tc.testRuntimeCELCostBudget = celconfig.RuntimeCELCostBudget | 				tc.testRuntimeCELCostBudget = celconfig.RuntimeCELCostBudget | ||||||
| 			} | 			} | ||||||
| 			optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer} | 			optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer} | ||||||
| 			ctx := context.TODO() | 			ctx := context.TODO() | ||||||
| 			evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, nil, tc.testRuntimeCELCostBudget) | 			evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)), optionalVars, nil, tc.testRuntimeCELCostBudget) | ||||||
|  | 			if tc.exceedPerCallLimit { | ||||||
|  | 				hasCostErr := false | ||||||
|  | 				for _, evalResult := range evalResults { | ||||||
|  | 					if evalResult.Error != nil && strings.Contains(evalResult.Error.Error(), "operation cancelled: actual cost limit exceeded") { | ||||||
|  | 						hasCostErr = true | ||||||
|  | 						break | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				if !hasCostErr { | ||||||
|  | 					t.Errorf("Expected per call limit exceeded error but didn't get one") | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 			if tc.exceedBudget && err == nil { | 			if tc.exceedBudget && err == nil { | ||||||
| 				t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil") | 				t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil") | ||||||
| 			} | 			} | ||||||
| 			if tc.exceedBudget && !strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") { | 			if tc.exceedBudget && err != nil && !strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") { | ||||||
| 				t.Errorf("Expected RuntimeCELCostBudge exceeded error but got: %v", err) | 				t.Errorf("Expected RuntimeCELCostBudge exceeded error but got: %v", err) | ||||||
| 			} | 			} | ||||||
| 			if err != nil && remaining != -1 { | 			if err != nil && remaining != -1 { | ||||||
|   | |||||||
| @@ -57,10 +57,12 @@ type OptionalVariableDeclarations struct { | |||||||
| 	// HasParams specifies if the "params" variable is declared. | 	// HasParams specifies if the "params" variable is declared. | ||||||
| 	// The "params" variable may still be bound to "null" when declared. | 	// The "params" variable may still be bound to "null" when declared. | ||||||
| 	HasParams bool | 	HasParams bool | ||||||
| 	// HasAuthorizer specifies if the"authorizer" and "authorizer.requestResource" | 	// HasAuthorizer specifies if the "authorizer" and "authorizer.requestResource" | ||||||
| 	// variables are declared. When declared, the authorizer variables are | 	// variables are declared. When declared, the authorizer variables are | ||||||
| 	// expected to be non-null. | 	// expected to be non-null. | ||||||
| 	HasAuthorizer bool | 	HasAuthorizer bool | ||||||
|  | 	// StrictCost specifies if the CEL cost limitation is strict for extended libraries as well as native libraries. | ||||||
|  | 	StrictCost bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values. | // FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values. | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ import ( | |||||||
| 	"k8s.io/apiserver/pkg/authorization/authorizer" | 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||||
| 	"k8s.io/apiserver/pkg/cel/environment" | 	"k8s.io/apiserver/pkg/cel/environment" | ||||||
| 	"k8s.io/apiserver/pkg/features" | 	"k8s.io/apiserver/pkg/features" | ||||||
|  | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
| 	"k8s.io/client-go/dynamic" | 	"k8s.io/client-go/dynamic" | ||||||
| 	"k8s.io/client-go/informers" | 	"k8s.io/client-go/informers" | ||||||
| 	"k8s.io/client-go/kubernetes" | 	"k8s.io/client-go/kubernetes" | ||||||
| @@ -43,13 +44,21 @@ const ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	compositionEnvTemplate *cel.CompositionEnv = func() *cel.CompositionEnv { | 	compositionEnvTemplateWithStrictCost *cel.CompositionEnv = func() *cel.CompositionEnv { | ||||||
| 		compositionEnvTemplate, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 		compositionEnvTemplateWithStrictCost, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			panic(err) | 			panic(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return compositionEnvTemplate | 		return compositionEnvTemplateWithStrictCost | ||||||
|  | 	}() | ||||||
|  | 	compositionEnvTemplateWithoutStrictCost *cel.CompositionEnv = func() *cel.CompositionEnv { | ||||||
|  | 		compositionEnvTemplateWithoutStrictCost, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), false)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			panic(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return compositionEnvTemplateWithoutStrictCost | ||||||
| 	}() | 	}() | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -114,12 +123,18 @@ func compilePolicy(policy *Policy) Validator { | |||||||
| 	if policy.Spec.ParamKind != nil { | 	if policy.Spec.ParamKind != nil { | ||||||
| 		hasParam = true | 		hasParam = true | ||||||
| 	} | 	} | ||||||
| 	optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true} | 	strictCost := utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP) | ||||||
| 	expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false} | 	optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true, StrictCost: strictCost} | ||||||
|  | 	expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false, StrictCost: strictCost} | ||||||
| 	failurePolicy := policy.Spec.FailurePolicy | 	failurePolicy := policy.Spec.FailurePolicy | ||||||
| 	var matcher matchconditions.Matcher = nil | 	var matcher matchconditions.Matcher = nil | ||||||
| 	matchConditions := policy.Spec.MatchConditions | 	matchConditions := policy.Spec.MatchConditions | ||||||
|  | 	var compositionEnvTemplate *cel.CompositionEnv | ||||||
|  | 	if strictCost { | ||||||
|  | 		compositionEnvTemplate = compositionEnvTemplateWithStrictCost | ||||||
|  | 	} else { | ||||||
|  | 		compositionEnvTemplate = compositionEnvTemplateWithoutStrictCost | ||||||
|  | 	} | ||||||
| 	filterCompiler := cel.NewCompositedCompilerFromTemplate(compositionEnvTemplate) | 	filterCompiler := cel.NewCompositedCompilerFromTemplate(compositionEnvTemplate) | ||||||
| 	filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions) | 	filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -39,6 +39,8 @@ import ( | |||||||
| 	"k8s.io/apiserver/pkg/cel/library" | 	"k8s.io/apiserver/pkg/cel/library" | ||||||
| 	"k8s.io/apiserver/pkg/cel/openapi" | 	"k8s.io/apiserver/pkg/cel/openapi" | ||||||
| 	"k8s.io/apiserver/pkg/cel/openapi/resolver" | 	"k8s.io/apiserver/pkg/cel/openapi/resolver" | ||||||
|  | 	"k8s.io/apiserver/pkg/features" | ||||||
|  | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -210,6 +212,7 @@ func (c *TypeChecker) CheckExpression(ctx *TypeCheckingContext, expression strin | |||||||
| 		options := plugincel.OptionalVariableDeclarations{ | 		options := plugincel.OptionalVariableDeclarations{ | ||||||
| 			HasParams:     ctx.paramDeclType != nil, | 			HasParams:     ctx.paramDeclType != nil, | ||||||
| 			HasAuthorizer: true, | 			HasAuthorizer: true, | ||||||
|  | 			StrictCost:    utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP), | ||||||
| 		} | 		} | ||||||
| 		compiler.CompileAndStoreVariables(convertv1beta1Variables(ctx.variables), options, environment.StoredExpressions) | 		compiler.CompileAndStoreVariables(convertv1beta1Variables(ctx.variables), options, environment.StoredExpressions) | ||||||
| 		result := compiler.CompileCELExpression(celExpression(expression), options, environment.StoredExpressions) | 		result := compiler.CompileCELExpression(celExpression(expression), options, environment.StoredExpressions) | ||||||
| @@ -391,7 +394,7 @@ func (c *TypeChecker) tryRefreshRESTMapper() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*environment.EnvSet, error) { | func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*environment.EnvSet, error) { | ||||||
| 	baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) | 	baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP)) | ||||||
| 	requestType := plugincel.BuildRequestType() | 	requestType := plugincel.BuildRequestType() | ||||||
| 	namespaceType := plugincel.BuildNamespaceType() | 	namespaceType := plugincel.BuildNamespaceType() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -931,7 +931,7 @@ func TestContextCanceled(t *testing.T) { | |||||||
|  |  | ||||||
| 	fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil) | 	fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil) | ||||||
| 	fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil) | 	fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil) | ||||||
| 	fc := cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	fc := cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
| 	f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions) | 	f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions) | ||||||
| 	v := validator{ | 	v := validator{ | ||||||
| 		failPolicy:       &fail, | 		failPolicy:       &fail, | ||||||
|   | |||||||
| @@ -27,6 +27,8 @@ import ( | |||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace" | 	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace" | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object" | 	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object" | ||||||
| 	"k8s.io/apiserver/pkg/cel/environment" | 	"k8s.io/apiserver/pkg/cel/environment" | ||||||
|  | 	"k8s.io/apiserver/pkg/features" | ||||||
|  | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
| 	webhookutil "k8s.io/apiserver/pkg/util/webhook" | 	webhookutil "k8s.io/apiserver/pkg/util/webhook" | ||||||
| 	"k8s.io/client-go/rest" | 	"k8s.io/client-go/rest" | ||||||
| ) | ) | ||||||
| @@ -139,11 +141,16 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler | |||||||
| 				Expression: matchCondition.Expression, | 				Expression: matchCondition.Expression, | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		strictCost := false | ||||||
|  | 		if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) { | ||||||
|  | 			strictCost = true | ||||||
|  | 		} | ||||||
| 		m.compiledMatcher = matchconditions.NewMatcher(compiler.Compile( | 		m.compiledMatcher = matchconditions.NewMatcher(compiler.Compile( | ||||||
| 			expressions, | 			expressions, | ||||||
| 			cel.OptionalVariableDeclarations{ | 			cel.OptionalVariableDeclarations{ | ||||||
| 				HasParams:     false, | 				HasParams:     false, | ||||||
| 				HasAuthorizer: true, | 				HasAuthorizer: true, | ||||||
|  | 				StrictCost:    strictCost, | ||||||
| 			}, | 			}, | ||||||
| 			environment.StoredExpressions, | 			environment.StoredExpressions, | ||||||
| 		), m.FailurePolicy, "webhook", "admit", m.Name) | 		), m.FailurePolicy, "webhook", "admit", m.Name) | ||||||
| @@ -267,11 +274,16 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil | |||||||
| 				Expression: matchCondition.Expression, | 				Expression: matchCondition.Expression, | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		strictCost := false | ||||||
|  | 		if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) { | ||||||
|  | 			strictCost = true | ||||||
|  | 		} | ||||||
| 		v.compiledMatcher = matchconditions.NewMatcher(compiler.Compile( | 		v.compiledMatcher = matchconditions.NewMatcher(compiler.Compile( | ||||||
| 			expressions, | 			expressions, | ||||||
| 			cel.OptionalVariableDeclarations{ | 			cel.OptionalVariableDeclarations{ | ||||||
| 				HasParams:     false, | 				HasParams:     false, | ||||||
| 				HasAuthorizer: true, | 				HasAuthorizer: true, | ||||||
|  | 				StrictCost:    strictCost, | ||||||
| 			}, | 			}, | ||||||
| 			environment.StoredExpressions, | 			environment.StoredExpressions, | ||||||
| 		), v.FailurePolicy, "webhook", "validating", v.Name) | 		), v.FailurePolicy, "webhook", "validating", v.Name) | ||||||
|   | |||||||
| @@ -21,7 +21,6 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
|  |  | ||||||
| 	admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" |  | ||||||
| 	"k8s.io/klog/v2" | 	"k8s.io/klog/v2" | ||||||
|  |  | ||||||
| 	admissionv1 "k8s.io/api/admission/v1" | 	admissionv1 "k8s.io/api/admission/v1" | ||||||
| @@ -31,6 +30,7 @@ import ( | |||||||
| 	"k8s.io/apimachinery/pkg/runtime/schema" | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
| 	"k8s.io/apiserver/pkg/admission" | 	"k8s.io/apiserver/pkg/admission" | ||||||
| 	genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer" | 	genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer" | ||||||
|  | 	admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/cel" | 	"k8s.io/apiserver/pkg/admission/plugin/cel" | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/webhook" | 	"k8s.io/apiserver/pkg/admission/plugin/webhook" | ||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/webhook/config" | 	"k8s.io/apiserver/pkg/admission/plugin/webhook/config" | ||||||
| @@ -39,6 +39,8 @@ import ( | |||||||
| 	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules" | 	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules" | ||||||
| 	"k8s.io/apiserver/pkg/authorization/authorizer" | 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||||
| 	"k8s.io/apiserver/pkg/cel/environment" | 	"k8s.io/apiserver/pkg/cel/environment" | ||||||
|  | 	"k8s.io/apiserver/pkg/features" | ||||||
|  | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
| 	webhookutil "k8s.io/apiserver/pkg/util/webhook" | 	webhookutil "k8s.io/apiserver/pkg/util/webhook" | ||||||
| 	"k8s.io/client-go/informers" | 	"k8s.io/client-go/informers" | ||||||
| 	clientset "k8s.io/client-go/kubernetes" | 	clientset "k8s.io/client-go/kubernetes" | ||||||
| @@ -100,7 +102,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory | |||||||
| 		namespaceMatcher: &namespace.Matcher{}, | 		namespaceMatcher: &namespace.Matcher{}, | ||||||
| 		objectMatcher:    &object.Matcher{}, | 		objectMatcher:    &object.Matcher{}, | ||||||
| 		dispatcher:       dispatcherFactory(&cm), | 		dispatcher:       dispatcherFactory(&cm), | ||||||
| 		filterCompiler:   cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())), | 		filterCompiler:   cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))), | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -91,7 +91,8 @@ func CompileAndValidateJWTAuthenticator(authenticator api.JWTAuthenticator, disa | |||||||
| func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) { | func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) { | ||||||
| 	var allErrs field.ErrorList | 	var allErrs field.ErrorList | ||||||
|  |  | ||||||
| 	compiler := authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	// strictCost is set to true which enables the strict cost for CEL validation. | ||||||
|  | 	compiler := authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
| 	state := &validationState{} | 	state := &validationState{} | ||||||
|  |  | ||||||
| 	allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"))...) | 	allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"))...) | ||||||
| @@ -722,7 +723,8 @@ func compileMatchConditions(matchConditions []api.WebhookMatchCondition, fldPath | |||||||
| 		return nil, allErrs | 		return nil, allErrs | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	compiler := authorizationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	// strictCost is set to true which enables the strict cost for CEL validation. | ||||||
|  | 	compiler := authorizationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
| 	seenExpressions := sets.NewString() | 	seenExpressions := sets.NewString() | ||||||
| 	var compilationResults []authorizationcel.CompilationResult | 	var compilationResults []authorizationcel.CompilationResult | ||||||
|  |  | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	compiler = authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	compiler = authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestValidateAuthenticationConfiguration(t *testing.T) { | func TestValidateAuthenticationConfiguration(t *testing.T) { | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ func TestCompileClaimsExpression(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
|  |  | ||||||
| 	for _, tc := range testCases { | 	for _, tc := range testCases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
| @@ -86,7 +86,7 @@ func TestCompileUserExpression(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
|  |  | ||||||
| 	for _, tc := range testCases { | 	for _, tc := range testCases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
| @@ -135,7 +135,7 @@ func TestCompileClaimsExpressionError(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
|  |  | ||||||
| 	for _, tc := range testCases { | 	for _, tc := range testCases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
| @@ -205,7 +205,7 @@ func TestCompileUserExpressionError(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
|  |  | ||||||
| 	for _, tc := range testCases { | 	for _, tc := range testCases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ func TestCompileCELExpression(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) | 	compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) | ||||||
|  |  | ||||||
| 	for _, tc := range cases { | 	for _, tc := range cases { | ||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|   | |||||||
| @@ -46,7 +46,9 @@ func DefaultCompatibilityVersion() *version.Version { | |||||||
| 	return version.MajorMinor(1, 29) | 	return version.MajorMinor(1, 29) | ||||||
| } | } | ||||||
|  |  | ||||||
| var baseOpts = []VersionedOptions{ | var baseOpts = append(baseOptsWithoutStrictCost, StrictCostOpt) | ||||||
|  |  | ||||||
|  | var baseOptsWithoutStrictCost = []VersionedOptions{ | ||||||
| 	{ | 	{ | ||||||
| 		// CEL epoch was actually 1.23, but we artificially set it to 1.0 because these | 		// CEL epoch was actually 1.23, but we artificially set it to 1.0 because these | ||||||
| 		// options should always be present. | 		// options should always be present. | ||||||
| @@ -139,6 +141,14 @@ var baseOpts = []VersionedOptions{ | |||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var StrictCostOpt = VersionedOptions{ | ||||||
|  | 	// This is to configure the cost calculation for extended libraries | ||||||
|  | 	IntroducedVersion: version.MajorMinor(1, 0), | ||||||
|  | 	ProgramOptions: []cel.ProgramOption{ | ||||||
|  | 		cel.CostTracking(&library.CostEstimator{}), | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  |  | ||||||
| // MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics | // MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics | ||||||
| // if the version is nil, or does not have major and minor components. | // if the version is nil, or does not have major and minor components. | ||||||
| // | // | ||||||
| @@ -148,7 +158,8 @@ var baseOpts = []VersionedOptions{ | |||||||
| // The returned environment contains no CEL variable definitions or custom type declarations and | // The returned environment contains no CEL variable definitions or custom type declarations and | ||||||
| // should be extended to construct environments with the appropriate variable definitions, | // should be extended to construct environments with the appropriate variable definitions, | ||||||
| // type declarations and any other needed configuration. | // type declarations and any other needed configuration. | ||||||
| func MustBaseEnvSet(ver *version.Version) *EnvSet { | // strictCost is used to determine whether to enforce strict cost calculation for CEL expressions. | ||||||
|  | func MustBaseEnvSet(ver *version.Version, strictCost bool) *EnvSet { | ||||||
| 	if ver == nil { | 	if ver == nil { | ||||||
| 		panic("version must be non-nil") | 		panic("version must be non-nil") | ||||||
| 	} | 	} | ||||||
| @@ -156,19 +167,33 @@ func MustBaseEnvSet(ver *version.Version) *EnvSet { | |||||||
| 		panic(fmt.Sprintf("version must contain an major and minor component, but got: %s", ver.String())) | 		panic(fmt.Sprintf("version must contain an major and minor component, but got: %s", ver.String())) | ||||||
| 	} | 	} | ||||||
| 	key := strconv.FormatUint(uint64(ver.Major()), 10) + "." + strconv.FormatUint(uint64(ver.Minor()), 10) | 	key := strconv.FormatUint(uint64(ver.Major()), 10) + "." + strconv.FormatUint(uint64(ver.Minor()), 10) | ||||||
| 	if entry, ok := baseEnvs.Load(key); ok { | 	var entry interface{} | ||||||
| 		return entry.(*EnvSet) | 	if strictCost { | ||||||
|  | 		if entry, ok := baseEnvs.Load(key); ok { | ||||||
|  | 			return entry.(*EnvSet) | ||||||
|  | 		} | ||||||
|  | 		entry, _, _ = baseEnvsSingleflight.Do(key, func() (interface{}, error) { | ||||||
|  | 			entry := mustNewEnvSet(ver, baseOpts) | ||||||
|  | 			baseEnvs.Store(key, entry) | ||||||
|  | 			return entry, nil | ||||||
|  | 		}) | ||||||
|  | 	} else { | ||||||
|  | 		if entry, ok := baseEnvsWithOption.Load(key); ok { | ||||||
|  | 			return entry.(*EnvSet) | ||||||
|  | 		} | ||||||
|  | 		entry, _, _ = baseEnvsWithOptionSingleflight.Do(key, func() (interface{}, error) { | ||||||
|  | 			entry := mustNewEnvSet(ver, baseOptsWithoutStrictCost) | ||||||
|  | 			baseEnvsWithOption.Store(key, entry) | ||||||
|  | 			return entry, nil | ||||||
|  | 		}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	entry, _, _ := baseEnvsSingleflight.Do(key, func() (interface{}, error) { |  | ||||||
| 		entry := mustNewEnvSet(ver, baseOpts) |  | ||||||
| 		baseEnvs.Store(key, entry) |  | ||||||
| 		return entry, nil |  | ||||||
| 	}) |  | ||||||
| 	return entry.(*EnvSet) | 	return entry.(*EnvSet) | ||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	baseEnvs             = sync.Map{} | 	baseEnvs                       = sync.Map{} | ||||||
| 	baseEnvsSingleflight = &singleflight.Group{} | 	baseEnvsWithOption             = sync.Map{} | ||||||
|  | 	baseEnvsSingleflight           = &singleflight.Group{} | ||||||
|  | 	baseEnvsWithOptionSingleflight = &singleflight.Group{} | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -29,10 +29,10 @@ import ( | |||||||
| // a cached environment is loaded for each MustBaseEnvSet call. | // a cached environment is loaded for each MustBaseEnvSet call. | ||||||
| func BenchmarkLoadBaseEnv(b *testing.B) { | func BenchmarkLoadBaseEnv(b *testing.B) { | ||||||
| 	ver := DefaultCompatibilityVersion() | 	ver := DefaultCompatibilityVersion() | ||||||
| 	MustBaseEnvSet(ver) | 	MustBaseEnvSet(ver, true) | ||||||
| 	b.ResetTimer() | 	b.ResetTimer() | ||||||
| 	for i := 0; i < b.N; i++ { | 	for i := 0; i < b.N; i++ { | ||||||
| 		MustBaseEnvSet(ver) | 		MustBaseEnvSet(ver, true) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -41,7 +41,7 @@ func BenchmarkLoadBaseEnv(b *testing.B) { | |||||||
| func BenchmarkLoadBaseEnvDifferentVersions(b *testing.B) { | func BenchmarkLoadBaseEnvDifferentVersions(b *testing.B) { | ||||||
| 	b.ResetTimer() | 	b.ResetTimer() | ||||||
| 	for i := 0; i < b.N; i++ { | 	for i := 0; i < b.N; i++ { | ||||||
| 		MustBaseEnvSet(version.MajorMinor(1, uint(i))) | 		MustBaseEnvSet(version.MajorMinor(1, uint(i)), true) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -262,7 +262,7 @@ func TestBaseEnvironment(t *testing.T) { | |||||||
| 			for _, tv := range tc.typeVersionCombinations { | 			for _, tv := range tc.typeVersionCombinations { | ||||||
| 				t.Run(fmt.Sprintf("version=%s,envType=%s", tv.version.String(), tv.envType), func(t *testing.T) { | 				t.Run(fmt.Sprintf("version=%s,envType=%s", tv.version.String(), tv.envType), func(t *testing.T) { | ||||||
|  |  | ||||||
| 					envSet := MustBaseEnvSet(tv.version) | 					envSet := MustBaseEnvSet(tv.version, true) | ||||||
| 					if tc.opts != nil { | 					if tc.opts != nil { | ||||||
| 						var err error | 						var err error | ||||||
| 						envSet, err = envSet.Extend(tc.opts...) | 						envSet, err = envSet.Extend(tc.opts...) | ||||||
|   | |||||||
| @@ -129,7 +129,7 @@ func compileAndRun(env *cel.Env, activation *testActivation, exp string) (ref.Va | |||||||
| func buildTestEnv() (*cel.Env, *apiservercel.DeclType, error) { | func buildTestEnv() (*cel.Env, *apiservercel.DeclType, error) { | ||||||
| 	variablesType := apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, 0) | 	variablesType := apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, 0) | ||||||
| 	variablesType.Fields = make(map[string]*apiservercel.DeclField) | 	variablesType.Fields = make(map[string]*apiservercel.DeclField) | ||||||
| 	envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend( | 	envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true).Extend( | ||||||
| 		environment.VersionedOptions{ | 		environment.VersionedOptions{ | ||||||
| 			IntroducedVersion: version.MajorMinor(1, 28), | 			IntroducedVersion: version.MajorMinor(1, 28), | ||||||
| 			EnvOptions: []cel.EnvOption{ | 			EnvOptions: []cel.EnvOption{ | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ import ( | |||||||
| // mustCreateEnv creates the default env for testing, with given option. | // mustCreateEnv creates the default env for testing, with given option. | ||||||
| // it fatally fails the test if the env fails to set up. | // it fatally fails the test if the env fails to set up. | ||||||
| func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { | func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { | ||||||
| 	envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()). | 	envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true). | ||||||
| 		Extend(environment.VersionedOptions{ | 		Extend(environment.VersionedOptions{ | ||||||
| 			IntroducedVersion: version.MajorMinor(1, 30), | 			IntroducedVersion: version.MajorMinor(1, 30), | ||||||
| 			EnvOptions:        envOptions, | 			EnvOptions:        envOptions, | ||||||
|   | |||||||
| @@ -140,7 +140,7 @@ func TestTypeProvider(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { | func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { | ||||||
| 	envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()). | 	envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true). | ||||||
| 		Extend(environment.VersionedOptions{ | 		Extend(environment.VersionedOptions{ | ||||||
| 			IntroducedVersion: version.MajorMinor(1, 30), | 			IntroducedVersion: version.MajorMinor(1, 30), | ||||||
| 			EnvOptions:        envOptions, | 			EnvOptions:        envOptions, | ||||||
|   | |||||||
| @@ -106,7 +106,7 @@ func buildTestEnv() (*cel.Env, error) { | |||||||
| 	fooType := common.SchemaDeclType(simpleMapSchema("foo", spec.StringProperty()), true).MaybeAssignTypeName("fooType") | 	fooType := common.SchemaDeclType(simpleMapSchema("foo", spec.StringProperty()), true).MaybeAssignTypeName("fooType") | ||||||
| 	barType := common.SchemaDeclType(simpleMapSchema("bar", spec.Int64Property()), true).MaybeAssignTypeName("barType") | 	barType := common.SchemaDeclType(simpleMapSchema("bar", spec.Int64Property()), true).MaybeAssignTypeName("barType") | ||||||
|  |  | ||||||
| 	env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend( | 	env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true).Extend( | ||||||
| 		environment.VersionedOptions{ | 		environment.VersionedOptions{ | ||||||
| 			IntroducedVersion: version.MajorMinor(1, 26), | 			IntroducedVersion: version.MajorMinor(1, 26), | ||||||
| 			EnvOptions: []cel.EnvOption{ | 			EnvOptions: []cel.EnvOption{ | ||||||
|   | |||||||
| @@ -220,6 +220,24 @@ const ( | |||||||
| 	// if the generated name conflicts with an existing resource name, up to a maximum number of 7 retries. | 	// if the generated name conflicts with an existing resource name, up to a maximum number of 7 retries. | ||||||
| 	RetryGenerateName featuregate.Feature = "RetryGenerateName" | 	RetryGenerateName featuregate.Feature = "RetryGenerateName" | ||||||
|  |  | ||||||
|  | 	// owner: @cici37 | ||||||
|  | 	// alpha: v1.30 | ||||||
|  | 	// | ||||||
|  | 	// StrictCostEnforcementForVAP is used to apply strict CEL cost validation for ValidatingAdmissionPolicy. | ||||||
|  | 	// It will be set to off by default for certain time of period to prevent the impact on the existing users. | ||||||
|  | 	// It is strongly recommended to enable this feature gate as early as possible. | ||||||
|  | 	// The strict cost is specific for the extended libraries whose cost defined under k8s/apiserver/pkg/cel/library. | ||||||
|  | 	StrictCostEnforcementForVAP featuregate.Feature = "StrictCostEnforcementForVAP" | ||||||
|  |  | ||||||
|  | 	// owner: @cici37 | ||||||
|  | 	// alpha: v1.30 | ||||||
|  | 	// | ||||||
|  | 	// StrictCostEnforcementForWebhooks is used to apply strict CEL cost validation for matchConditions in Webhooks. | ||||||
|  | 	// It will be set to off by default for certain time of period to prevent the impact on the existing users. | ||||||
|  | 	// It is strongly recommended to enable this feature gate as early as possible. | ||||||
|  | 	// The strict cost is specific for the extended libraries whose cost defined under k8s/apiserver/pkg/cel/library. | ||||||
|  | 	StrictCostEnforcementForWebhooks featuregate.Feature = "StrictCostEnforcementForWebhooks" | ||||||
|  |  | ||||||
| 	// owner: @caesarxuchao @roycaihw | 	// owner: @caesarxuchao @roycaihw | ||||||
| 	// alpha: v1.20 | 	// alpha: v1.20 | ||||||
| 	// | 	// | ||||||
| @@ -347,6 +365,10 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS | |||||||
|  |  | ||||||
| 	StorageVersionHash: {Default: true, PreRelease: featuregate.Beta}, | 	StorageVersionHash: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
|  | 	StrictCostEnforcementForVAP: {Default: false, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
|  | 	StrictCostEnforcementForWebhooks: {Default: false, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
| 	StructuredAuthenticationConfiguration: {Default: true, PreRelease: featuregate.Beta}, | 	StructuredAuthenticationConfiguration: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
| 	StructuredAuthorizationConfiguration: {Default: true, PreRelease: featuregate.Beta}, | 	StructuredAuthorizationConfiguration: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|   | |||||||
| @@ -184,7 +184,8 @@ func (c CompilationResult) Evaluate(ctx context.Context, attributes []resourceap | |||||||
| } | } | ||||||
|  |  | ||||||
| func mustBuildEnv() *environment.EnvSet { | func mustBuildEnv() *environment.EnvSet { | ||||||
| 	envset := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) | 	// strictCost is always true to enforce cost limits. | ||||||
|  | 	envset := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true) | ||||||
| 	versioned := []environment.VersionedOptions{ | 	versioned := []environment.VersionedOptions{ | ||||||
| 		{ | 		{ | ||||||
| 			// Feature epoch was actually 1.30, but we artificially set it to 1.0 because these | 			// Feature epoch was actually 1.30, but we artificially set it to 1.0 because these | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -36,7 +37,10 @@ import ( | |||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	genericfeatures "k8s.io/apiserver/pkg/features" | ||||||
|  | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
| 	clientset "k8s.io/client-go/kubernetes" | 	clientset "k8s.io/client-go/kubernetes" | ||||||
|  | 	featuregatetesting "k8s.io/component-base/featuregate/testing" | ||||||
| 	apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" | 	apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" | ||||||
| 	"k8s.io/kubernetes/test/integration/framework" | 	"k8s.io/kubernetes/test/integration/framework" | ||||||
| ) | ) | ||||||
| @@ -107,7 +111,7 @@ func newMatchConditionHandler(recorder *admissionRecorder) http.Handler { | |||||||
|  |  | ||||||
| // TestMatchConditions tests ValidatingWebhookConfigurations and MutatingWebhookConfigurations that validates different cases of matchCondition fields | // TestMatchConditions tests ValidatingWebhookConfigurations and MutatingWebhookConfigurations that validates different cases of matchCondition fields | ||||||
| func TestMatchConditions(t *testing.T) { | func TestMatchConditions(t *testing.T) { | ||||||
|  | 	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.StrictCostEnforcementForWebhooks, false) | ||||||
| 	fail := admissionregistrationv1.Fail | 	fail := admissionregistrationv1.Fail | ||||||
| 	ignore := admissionregistrationv1.Ignore | 	ignore := admissionregistrationv1.Ignore | ||||||
|  |  | ||||||
| @@ -118,6 +122,7 @@ func TestMatchConditions(t *testing.T) { | |||||||
| 		matchedPods     []*corev1.Pod | 		matchedPods     []*corev1.Pod | ||||||
| 		expectErrorPod  bool | 		expectErrorPod  bool | ||||||
| 		failPolicy      *admissionregistrationv1.FailurePolicyType | 		failPolicy      *admissionregistrationv1.FailurePolicyType | ||||||
|  | 		errMessage      string | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name: "pods in namespace kube-system is ignored", | 			name: "pods in namespace kube-system is ignored", | ||||||
| @@ -284,6 +289,35 @@ func TestMatchConditions(t *testing.T) { | |||||||
| 				matchConditionsTestPod("test2", "default"), | 				matchConditionsTestPod("test2", "default"), | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "without strict cost enforcement: Authz check does not exceed per call limit", | ||||||
|  | 			matchConditions: []admissionregistrationv1.MatchCondition{ | ||||||
|  | 				{ | ||||||
|  | 					Name:       "test1", | ||||||
|  | 					Expression: "authorizer.group('').resource('pods').name('test1').check('create').allowed() && authorizer.group('').resource('pods').name('test1').check('create').allowed() && authorizer.group('').resource('pods').name('test1').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			pods: []*corev1.Pod{ | ||||||
|  | 				matchConditionsTestPod("test1", "kube-system"), | ||||||
|  | 			}, | ||||||
|  | 			matchedPods: []*corev1.Pod{ | ||||||
|  | 				matchConditionsTestPod("test1", "kube-system"), | ||||||
|  | 			}, | ||||||
|  | 			failPolicy:     &fail, | ||||||
|  | 			expectErrorPod: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:            "without strict cost enforcement: Authz check does not exceed overall cost limit", | ||||||
|  | 			matchConditions: generateMatchConditionsWithAuthzCheck(8, "authorizer.group('').resource('pods').name('test1').check('create').allowed() && authorizer.group('').resource('pods').name('test1').check('create').allowed()"), | ||||||
|  | 			pods: []*corev1.Pod{ | ||||||
|  | 				matchConditionsTestPod("test1", "kube-system"), | ||||||
|  | 			}, | ||||||
|  | 			matchedPods: []*corev1.Pod{ | ||||||
|  | 				matchConditionsTestPod("test1", "kube-system"), | ||||||
|  | 			}, | ||||||
|  | 			failPolicy:     &fail, | ||||||
|  | 			expectErrorPod: false, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	roots := x509.NewCertPool() | 	roots := x509.NewCertPool() | ||||||
| @@ -432,6 +466,8 @@ func TestMatchConditions(t *testing.T) { | |||||||
| 					t.Fatalf("unexpected error creating test pod: %v", err) | 					t.Fatalf("unexpected error creating test pod: %v", err) | ||||||
| 				} else if testcase.expectErrorPod && err == nil { | 				} else if testcase.expectErrorPod && err == nil { | ||||||
| 					t.Fatal("expected error creating pods") | 					t.Fatal("expected error creating pods") | ||||||
|  | 				} else if testcase.expectErrorPod && err != nil && !strings.Contains(err.Error(), testcase.errMessage) { | ||||||
|  | 					t.Fatalf("expected error message includes: %v, but get %v", testcase.errMessage, err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -448,8 +484,324 @@ func TestMatchConditions(t *testing.T) { | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			//Reset and rerun against mutating webhook configuration | 			// Reset and rerun against mutating webhook configuration | ||||||
| 			//TODO: private helper function for validation after creating vwh or mwh | 			// TODO: private helper function for validation after creating vwh or mwh | ||||||
|  | 			upCh = recorder.Reset() | ||||||
|  | 			err = client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(context.TODO(), validatingcfg.GetName(), metav1.DeleteOptions{}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} else { | ||||||
|  | 				vhwHasBeenCleanedUp = true | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			mutatingwebhook := &admissionregistrationv1.MutatingWebhookConfiguration{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "admission.integration.test", | ||||||
|  | 				}, | ||||||
|  | 				Webhooks: []admissionregistrationv1.MutatingWebhook{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "admission.integration.test", | ||||||
|  | 						Rules: []admissionregistrationv1.RuleWithOperations{{ | ||||||
|  | 							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, | ||||||
|  | 							Rule: admissionregistrationv1.Rule{ | ||||||
|  | 								APIGroups:   []string{""}, | ||||||
|  | 								APIVersions: []string{"v1"}, | ||||||
|  | 								Resources:   []string{"pods"}, | ||||||
|  | 							}, | ||||||
|  | 						}}, | ||||||
|  | 						ClientConfig: admissionregistrationv1.WebhookClientConfig{ | ||||||
|  | 							URL:      &endpoint, | ||||||
|  | 							CABundle: localhostCert, | ||||||
|  | 						}, | ||||||
|  | 						// ignore pods in the marker namespace | ||||||
|  | 						NamespaceSelector: &metav1.LabelSelector{ | ||||||
|  | 							MatchExpressions: []metav1.LabelSelectorRequirement{ | ||||||
|  | 								{ | ||||||
|  | 									Key:      corev1.LabelMetadataName, | ||||||
|  | 									Operator: metav1.LabelSelectorOpNotIn, | ||||||
|  | 									Values:   []string{"marker"}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 						FailurePolicy:           testcase.failPolicy, | ||||||
|  | 						SideEffects:             &noSideEffects, | ||||||
|  | 						AdmissionReviewVersions: []string{"v1"}, | ||||||
|  | 						MatchConditions:         testcase.matchConditions, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Name: "admission.integration.test.marker", | ||||||
|  | 						Rules: []admissionregistrationv1.RuleWithOperations{{ | ||||||
|  | 							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, | ||||||
|  | 							Rule:       admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}}, | ||||||
|  | 						}}, | ||||||
|  | 						ClientConfig: admissionregistrationv1.WebhookClientConfig{ | ||||||
|  | 							URL:      &markerEndpoint, | ||||||
|  | 							CABundle: localhostCert, | ||||||
|  | 						}, | ||||||
|  | 						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{ | ||||||
|  | 							corev1.LabelMetadataName: "marker", | ||||||
|  | 						}}, | ||||||
|  | 						ObjectSelector:          &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}}, | ||||||
|  | 						FailurePolicy:           testcase.failPolicy, | ||||||
|  | 						SideEffects:             &noSideEffects, | ||||||
|  | 						AdmissionReviewVersions: []string{"v1"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			mutatingcfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), mutatingwebhook, metav1.CreateOptions{}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			defer func() { | ||||||
|  | 				err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingcfg.GetName(), metav1.DeleteOptions{}) | ||||||
|  | 				if err != nil { | ||||||
|  | 					t.Fatal(err) | ||||||
|  | 				} | ||||||
|  | 			}() | ||||||
|  |  | ||||||
|  | 			// wait until new webhook is called the first time | ||||||
|  | 			if err := wait.PollUntilContextTimeout(context.Background(), time.Millisecond*5, wait.ForeverTestTimeout, true, func(_ context.Context) (bool, error) { | ||||||
|  | 				_, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{}) | ||||||
|  | 				select { | ||||||
|  | 				case <-upCh: | ||||||
|  | 					return true, nil | ||||||
|  | 				default: | ||||||
|  | 					t.Logf("Waiting for webhook to become effective, getting marker object: %v", err) | ||||||
|  | 					return false, nil | ||||||
|  | 				} | ||||||
|  | 			}); err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			for _, pod := range testcase.pods { | ||||||
|  | 				_, err = client.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, dryRunCreate) | ||||||
|  | 				if testcase.expectErrorPod == false && err != nil { | ||||||
|  | 					t.Fatalf("unexpected error creating test pod: %v", err) | ||||||
|  | 				} else if testcase.expectErrorPod == true && err == nil { | ||||||
|  | 					t.Fatal("expected error creating pods") | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if len(recorder.requests) != len(testcase.matchedPods) { | ||||||
|  | 				t.Errorf("unexpected requests %v, expected %v", recorder.requests, testcase.matchedPods) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			for i, request := range recorder.requests { | ||||||
|  | 				if request.Name != testcase.matchedPods[i].Name { | ||||||
|  | 					t.Errorf("unexpected pod name %v, expected %v", request.Name, testcase.matchedPods[i].Name) | ||||||
|  | 				} | ||||||
|  | 				if request.Namespace != testcase.matchedPods[i].Namespace { | ||||||
|  | 					t.Errorf("unexpected pod namespace %v, expected %v", request.Namespace, testcase.matchedPods[i].Namespace) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMatchConditionsWithStrictCostEnforcement(t *testing.T) { | ||||||
|  | 	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.StrictCostEnforcementForWebhooks, true) | ||||||
|  |  | ||||||
|  | 	testcases := []struct { | ||||||
|  | 		name            string | ||||||
|  | 		matchConditions []admissionregistrationv1.MatchCondition | ||||||
|  | 		pods            []*corev1.Pod | ||||||
|  | 		matchedPods     []*corev1.Pod | ||||||
|  | 		expectErrorPod  bool | ||||||
|  | 		failPolicy      *admissionregistrationv1.FailurePolicyType | ||||||
|  | 		errMessage      string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "with strict cost enforcement: exceed per call limit should reject request with fail policy fail", | ||||||
|  | 			matchConditions: []admissionregistrationv1.MatchCondition{ | ||||||
|  | 				{ | ||||||
|  | 					Name:       "test1", | ||||||
|  | 					Expression: "authorizer.group('').resource('pods').namespace('default').check('create').allowed() && authorizer.group('').resource('pods').namespace('default').check('create').allowed() && authorizer.group('').resource('pods').namespace('default').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			pods: []*corev1.Pod{ | ||||||
|  | 				matchConditionsTestPod("test1", "default"), | ||||||
|  | 			}, | ||||||
|  | 			matchedPods:    []*corev1.Pod{}, | ||||||
|  | 			expectErrorPod: true, | ||||||
|  | 			errMessage:     "operation cancelled: actual cost limit exceeded", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:            "with strict cost enforcement: exceed overall cost limit should reject request with fail policy fail", | ||||||
|  | 			matchConditions: generateMatchConditionsWithAuthzCheck(8, "authorizer.group('').resource('pods').name('test1').check('create').allowed() && authorizer.group('').resource('pods').name('test1').check('create').allowed()"), | ||||||
|  | 			pods: []*corev1.Pod{ | ||||||
|  | 				matchConditionsTestPod("test1", "kube-system"), | ||||||
|  | 			}, | ||||||
|  | 			matchedPods:    []*corev1.Pod{}, | ||||||
|  | 			expectErrorPod: true, | ||||||
|  | 			errMessage:     "validation failed due to running out of cost budget, no further validation rules will be run", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	roots := x509.NewCertPool() | ||||||
|  | 	if !roots.AppendCertsFromPEM(localhostCert) { | ||||||
|  | 		t.Fatal("Failed to append Cert from PEM") | ||||||
|  | 	} | ||||||
|  | 	cert, err := tls.X509KeyPair(localhostCert, localhostKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to build cert with error: %+v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	recorder := &admissionRecorder{requests: []*admissionv1.AdmissionRequest{}} | ||||||
|  |  | ||||||
|  | 	webhookServer := httptest.NewUnstartedServer(newMatchConditionHandler(recorder)) | ||||||
|  | 	webhookServer.TLS = &tls.Config{ | ||||||
|  | 		RootCAs:      roots, | ||||||
|  | 		Certificates: []tls.Certificate{cert}, | ||||||
|  | 	} | ||||||
|  | 	webhookServer.StartTLS() | ||||||
|  | 	defer webhookServer.Close() | ||||||
|  |  | ||||||
|  | 	dryRunCreate := metav1.CreateOptions{ | ||||||
|  | 		DryRun: []string{metav1.DryRunAll}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, testcase := range testcases { | ||||||
|  | 		t.Run(testcase.name, func(t *testing.T) { | ||||||
|  | 			upCh := recorder.Reset() | ||||||
|  | 			server, err := apiservertesting.StartTestServer(t, nil, []string{ | ||||||
|  | 				"--disable-admission-plugins=ServiceAccount", | ||||||
|  | 			}, framework.SharedEtcd()) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			defer server.TearDownFn() | ||||||
|  |  | ||||||
|  | 			config := server.ClientConfig | ||||||
|  |  | ||||||
|  | 			client, err := clientset.NewForConfig(config) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Write markers to a separate namespace to avoid cross-talk | ||||||
|  | 			markerNs := "marker" | ||||||
|  | 			_, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: markerNs}}, metav1.CreateOptions{}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Create a marker object to use to check for the webhook configurations to be ready. | ||||||
|  | 			marker, err := client.CoreV1().Pods(markerNs).Create(context.TODO(), newMarkerPod(markerNs), metav1.CreateOptions{}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			endpoint := webhookServer.URL | ||||||
|  | 			markerEndpoint := webhookServer.URL + "/marker" | ||||||
|  | 			validatingwebhook := &admissionregistrationv1.ValidatingWebhookConfiguration{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "admission.integration.test", | ||||||
|  | 				}, | ||||||
|  | 				Webhooks: []admissionregistrationv1.ValidatingWebhook{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "admission.integration.test", | ||||||
|  | 						Rules: []admissionregistrationv1.RuleWithOperations{{ | ||||||
|  | 							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, | ||||||
|  | 							Rule: admissionregistrationv1.Rule{ | ||||||
|  | 								APIGroups:   []string{""}, | ||||||
|  | 								APIVersions: []string{"v1"}, | ||||||
|  | 								Resources:   []string{"pods"}, | ||||||
|  | 							}, | ||||||
|  | 						}}, | ||||||
|  | 						ClientConfig: admissionregistrationv1.WebhookClientConfig{ | ||||||
|  | 							URL:      &endpoint, | ||||||
|  | 							CABundle: localhostCert, | ||||||
|  | 						}, | ||||||
|  | 						// ignore pods in the marker namespace | ||||||
|  | 						NamespaceSelector: &metav1.LabelSelector{ | ||||||
|  | 							MatchExpressions: []metav1.LabelSelectorRequirement{ | ||||||
|  | 								{ | ||||||
|  | 									Key:      corev1.LabelMetadataName, | ||||||
|  | 									Operator: metav1.LabelSelectorOpNotIn, | ||||||
|  | 									Values:   []string{"marker"}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 						FailurePolicy:           testcase.failPolicy, | ||||||
|  | 						SideEffects:             &noSideEffects, | ||||||
|  | 						AdmissionReviewVersions: []string{"v1"}, | ||||||
|  | 						MatchConditions:         testcase.matchConditions, | ||||||
|  | 					}, | ||||||
|  | 					{ | ||||||
|  | 						Name: "admission.integration.test.marker", | ||||||
|  | 						Rules: []admissionregistrationv1.RuleWithOperations{{ | ||||||
|  | 							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, | ||||||
|  | 							Rule:       admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}}, | ||||||
|  | 						}}, | ||||||
|  | 						ClientConfig: admissionregistrationv1.WebhookClientConfig{ | ||||||
|  | 							URL:      &markerEndpoint, | ||||||
|  | 							CABundle: localhostCert, | ||||||
|  | 						}, | ||||||
|  | 						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{ | ||||||
|  | 							corev1.LabelMetadataName: "marker", | ||||||
|  | 						}}, | ||||||
|  | 						ObjectSelector:          &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}}, | ||||||
|  | 						FailurePolicy:           testcase.failPolicy, | ||||||
|  | 						SideEffects:             &noSideEffects, | ||||||
|  | 						AdmissionReviewVersions: []string{"v1"}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			validatingcfg, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), validatingwebhook, metav1.CreateOptions{}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			vhwHasBeenCleanedUp := false | ||||||
|  | 			defer func() { | ||||||
|  | 				if !vhwHasBeenCleanedUp { | ||||||
|  | 					err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(context.TODO(), validatingcfg.GetName(), metav1.DeleteOptions{}) | ||||||
|  | 					if err != nil { | ||||||
|  | 						t.Fatal(err) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}() | ||||||
|  |  | ||||||
|  | 			// wait until new webhook is called the first time | ||||||
|  | 			if err := wait.PollUntilContextTimeout(context.Background(), time.Millisecond*5, wait.ForeverTestTimeout, true, func(_ context.Context) (bool, error) { | ||||||
|  | 				_, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{}) | ||||||
|  | 				select { | ||||||
|  | 				case <-upCh: | ||||||
|  | 					return true, nil | ||||||
|  | 				default: | ||||||
|  | 					t.Logf("Waiting for webhook to become effective, getting marker object: %v", err) | ||||||
|  | 					return false, nil | ||||||
|  | 				} | ||||||
|  | 			}); err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			for _, pod := range testcase.pods { | ||||||
|  | 				_, err := client.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, dryRunCreate) | ||||||
|  | 				if !testcase.expectErrorPod && err != nil { | ||||||
|  | 					t.Fatalf("unexpected error creating test pod: %v", err) | ||||||
|  | 				} else if testcase.expectErrorPod && err == nil { | ||||||
|  | 					t.Fatal("expected error creating pods") | ||||||
|  | 				} else if testcase.expectErrorPod && err != nil && !strings.Contains(err.Error(), testcase.errMessage) { | ||||||
|  | 					t.Fatalf("expected error message includes: %v, but get %v", testcase.errMessage, err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if len(recorder.requests) != len(testcase.matchedPods) { | ||||||
|  | 				t.Errorf("unexpected requests %v, expected %v", recorder.requests, testcase.matchedPods) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			for i, request := range recorder.requests { | ||||||
|  | 				if request.Name != testcase.matchedPods[i].Name { | ||||||
|  | 					t.Errorf("unexpected pod name %v, expected %v", request.Name, testcase.matchedPods[i].Name) | ||||||
|  | 				} | ||||||
|  | 				if request.Namespace != testcase.matchedPods[i].Namespace { | ||||||
|  | 					t.Errorf("unexpected pod namespace %v, expected %v", request.Namespace, testcase.matchedPods[i].Namespace) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Reset and rerun against mutating webhook configuration | ||||||
|  | 			// TODO: private helper function for validation after creating vwh or mwh | ||||||
| 			upCh = recorder.Reset() | 			upCh = recorder.Reset() | ||||||
| 			err = client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(context.TODO(), validatingcfg.GetName(), metav1.DeleteOptions{}) | 			err = client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(context.TODO(), validatingcfg.GetName(), metav1.DeleteOptions{}) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -777,3 +1129,13 @@ func repeatedMatchConditions(size int) []admissionregistrationv1.MatchCondition | |||||||
| 	} | 	} | ||||||
| 	return matchConditions | 	return matchConditions | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // generate n matchConditions with provided expression | ||||||
|  | func generateMatchConditionsWithAuthzCheck(num int, exp string) []admissionregistrationv1.MatchCondition { | ||||||
|  | 	var conditions = make([]admissionregistrationv1.MatchCondition, num) | ||||||
|  | 	for i := 0; i < num; i++ { | ||||||
|  | 		conditions[i].Name = "test" + strconv.Itoa(i) | ||||||
|  | 		conditions[i].Expression = exp | ||||||
|  | 	} | ||||||
|  | 	return conditions | ||||||
|  | } | ||||||
|   | |||||||
| @@ -376,7 +376,7 @@ func TestBuiltinResolution(t *testing.T) { | |||||||
| // with the practical defaults. | // with the practical defaults. | ||||||
| // `self` is defined as the object being evaluated against. | // `self` is defined as the object being evaluated against. | ||||||
| func simpleCompileCEL(schema *spec.Schema, expression string) (cel.Program, error) { | func simpleCompileCEL(schema *spec.Schema, expression string) (cel.Program, error) { | ||||||
| 	env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Env(environment.NewExpressions) | 	env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true).Env(environment.NewExpressions) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -2131,6 +2131,217 @@ func Test_ValidatingAdmissionPolicy_ParamResourceDeletedThenRecreated(t *testing | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Test_CostLimitForValidation tests the cost limit set for a ValidatingAdmissionPolicy. | ||||||
|  | func Test_CostLimitForValidation(t *testing.T) { | ||||||
|  | 	testcases := []struct { | ||||||
|  | 		name                  string | ||||||
|  | 		policy                *admissionregistrationv1.ValidatingAdmissionPolicy | ||||||
|  | 		policyBinding         *admissionregistrationv1.ValidatingAdmissionPolicyBinding | ||||||
|  | 		namespace             *v1.Namespace | ||||||
|  | 		err                   string | ||||||
|  | 		failureReason         metav1.StatusReason | ||||||
|  | 		strictCostEnforcement bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP: Single expression exceeds per call cost limit for native library", | ||||||
|  | 			policy: withValidations([]admissionregistrationv1.Validation{ | ||||||
|  | 				{ | ||||||
|  | 					Expression: "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(x, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(y, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z3, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z4, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z5, int('1'.find('[0-9]*')) < 100)))))))", | ||||||
|  | 				}, | ||||||
|  | 			}, withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			err:                   "operation cancelled: actual cost limit exceeded", | ||||||
|  | 			failureReason:         metav1.StatusReasonInvalid, | ||||||
|  | 			strictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP: Expression exceeds per call cost limit for extended library", | ||||||
|  | 			policy: withValidations([]admissionregistrationv1.Validation{ | ||||||
|  | 				{ | ||||||
|  |  | ||||||
|  | 					Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			err:                   "operation cancelled: actual cost limit exceeded", | ||||||
|  | 			failureReason:         metav1.StatusReasonInvalid, | ||||||
|  | 			strictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP: Expression exceeds per call cost limit for extended library in variables", | ||||||
|  | 			policy: withVariables([]admissionregistrationv1.Variable{ | ||||||
|  | 				{ | ||||||
|  | 					Name:       "authzCheck", | ||||||
|  | 					Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, withValidations([]admissionregistrationv1.Validation{ | ||||||
|  | 				{ | ||||||
|  | 					Expression: "variables.authzCheck", | ||||||
|  | 				}, | ||||||
|  | 			}, withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix"))))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			err:                   "operation cancelled: actual cost limit exceeded", | ||||||
|  | 			failureReason:         metav1.StatusReasonInvalid, | ||||||
|  | 			strictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "With StrictCostEnforcementForVAP: Expression exceeds per call cost limit for extended library in matchConditions", | ||||||
|  | 			policy: withMatchConditions([]admissionregistrationv1.MatchCondition{ | ||||||
|  | 				{ | ||||||
|  | 					Name:       "test", | ||||||
|  | 					Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, withValidations([]admissionregistrationv1.Validation{ | ||||||
|  | 				{ | ||||||
|  | 					Expression: "true", | ||||||
|  | 				}, | ||||||
|  | 			}, withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix"))))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			err:                   "operation cancelled: actual cost limit exceeded", | ||||||
|  | 			failureReason:         metav1.StatusReasonInvalid, | ||||||
|  | 			strictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:          "With StrictCostEnforcementForVAP: Expression exceeds per policy cost limit for extended library", | ||||||
|  | 			policy:        withValidations(generateValidationsWithAuthzCheck(29, "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()"), withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			err:                   "validation failed due to running out of cost budget, no further validation rules will be run", | ||||||
|  | 			failureReason:         metav1.StatusReasonInvalid, | ||||||
|  | 			strictCostEnforcement: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Without StrictCostEnforcementForVAP: Single expression exceeds per call cost limit for native library", | ||||||
|  | 			policy: withValidations([]admissionregistrationv1.Validation{ | ||||||
|  | 				{ | ||||||
|  | 					Expression: "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(x, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(y, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z3, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z4, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z5, int('1'.find('[0-9]*')) < 100)))))))", | ||||||
|  | 				}, | ||||||
|  | 			}, withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			err:                   "operation cancelled: actual cost limit exceeded", | ||||||
|  | 			failureReason:         metav1.StatusReasonInvalid, | ||||||
|  | 			strictCostEnforcement: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Without StrictCostEnforcementForVAP: Expression does not exceed per call cost limit for extended library", | ||||||
|  | 			policy: withValidations([]admissionregistrationv1.Validation{ | ||||||
|  | 				{ | ||||||
|  |  | ||||||
|  | 					Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			strictCostEnforcement: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Without StrictCostEnforcementForVAP: Expression does not exceed per call cost limit for extended library in variables", | ||||||
|  | 			policy: withVariables([]admissionregistrationv1.Variable{ | ||||||
|  | 				{ | ||||||
|  | 					Name:       "authzCheck", | ||||||
|  | 					Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed() && authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", | ||||||
|  | 				}, | ||||||
|  | 			}, withValidations([]admissionregistrationv1.Validation{ | ||||||
|  | 				{ | ||||||
|  | 					Expression: "variables.authzCheck", | ||||||
|  | 				}, | ||||||
|  | 			}, withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix"))))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			strictCostEnforcement: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:          "Without StrictCostEnforcementForVAP: Expression does not exceed per policy cost limit for extended library", | ||||||
|  | 			policy:        withValidations(generateValidationsWithAuthzCheck(29, "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()"), withFailurePolicy(admissionregistrationv1.Fail, withNamespaceMatch(makePolicy("validate-namespace-suffix")))), | ||||||
|  | 			policyBinding: makeBinding("validate-namespace-suffix-binding", "validate-namespace-suffix", ""), | ||||||
|  | 			namespace: &v1.Namespace{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "test-k8s", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			strictCostEnforcement: false, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, testcase := range testcases { | ||||||
|  | 		t.Run(testcase.name, func(t *testing.T) { | ||||||
|  | 			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.StrictCostEnforcementForVAP, testcase.strictCostEnforcement) | ||||||
|  |  | ||||||
|  | 			server, err := apiservertesting.StartTestServer(t, nil, []string{ | ||||||
|  | 				"--enable-admission-plugins", "ValidatingAdmissionPolicy", | ||||||
|  | 			}, framework.SharedEtcd()) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			defer server.TearDownFn() | ||||||
|  |  | ||||||
|  | 			config := server.ClientConfig | ||||||
|  |  | ||||||
|  | 			client, err := clientset.NewForConfig(config) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			policy := withWaitReadyConstraintAndExpression(testcase.policy) | ||||||
|  | 			if _, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(context.TODO(), policy, metav1.CreateOptions{}); err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 			if err := createAndWaitReady(t, client, testcase.policyBinding, nil); err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			_, err = client.CoreV1().Namespaces().Create(context.TODO(), testcase.namespace, metav1.CreateOptions{}) | ||||||
|  |  | ||||||
|  | 			checkExpectedError(t, err, testcase.err) | ||||||
|  | 			checkFailureReason(t, err, testcase.failureReason) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // generate n validation rules with provided expression | ||||||
|  | func generateValidationsWithAuthzCheck(num int, exp string) []admissionregistrationv1.Validation { | ||||||
|  | 	var validations = make([]admissionregistrationv1.Validation, num) | ||||||
|  | 	for i := 0; i < num; i++ { | ||||||
|  | 		validations[i].Expression = exp | ||||||
|  | 	} | ||||||
|  | 	return validations | ||||||
|  | } | ||||||
|  |  | ||||||
| // TestCRDParams tests that a CustomResource can be used as a param resource for a ValidatingAdmissionPolicy. | // TestCRDParams tests that a CustomResource can be used as a param resource for a ValidatingAdmissionPolicy. | ||||||
| func TestCRDParams(t *testing.T) { | func TestCRDParams(t *testing.T) { | ||||||
| 	testcases := []struct { | 	testcases := []struct { | ||||||
| @@ -2722,6 +2933,12 @@ func createAndWaitReadyNamespacedWithWarnHandler(t *testing.T, client clientset. | |||||||
| 		} else if err != nil && strings.Contains(err.Error(), "not yet synced to use for admission") { | 		} else if err != nil && strings.Contains(err.Error(), "not yet synced to use for admission") { | ||||||
| 			t.Logf("waiting for policy to be ready. Marker: %v. Admission not synced yet: %v", marker, err) | 			t.Logf("waiting for policy to be ready. Marker: %v. Admission not synced yet: %v", marker, err) | ||||||
| 			return false, nil | 			return false, nil | ||||||
|  | 		} else if err != nil && strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") { | ||||||
|  | 			t.Logf("policy is ready and exceeds the budget: %v", err) | ||||||
|  | 			return true, nil | ||||||
|  | 		} else if err != nil && strings.Contains(err.Error(), "operation cancelled: actual cost limit exceeded") { | ||||||
|  | 			t.Logf("policy is ready and exceeds the budget: %v", err) | ||||||
|  | 			return true, nil | ||||||
| 		} else { | 		} else { | ||||||
| 			t.Logf("waiting for policy to be ready. Marker: %v, Last marker patch response: %v", marker, err) | 			t.Logf("waiting for policy to be ready. Marker: %v, Last marker patch response: %v", marker, err) | ||||||
| 			return false, err | 			return false, err | ||||||
| @@ -2856,6 +3073,16 @@ func withValidations(validations []admissionregistrationv1.Validation, policy *a | |||||||
| 	return policy | 	return policy | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func withVariables(variables []admissionregistrationv1.Variable, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { | ||||||
|  | 	policy.Spec.Variables = variables | ||||||
|  | 	return policy | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func withMatchConditions(matchConditions []admissionregistrationv1.MatchCondition, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { | ||||||
|  | 	policy.Spec.MatchConditions = matchConditions | ||||||
|  | 	return policy | ||||||
|  | } | ||||||
|  |  | ||||||
| func withAuditAnnotations(auditAnnotations []admissionregistrationv1.AuditAnnotation, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { | func withAuditAnnotations(auditAnnotations []admissionregistrationv1.AuditAnnotation, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { | ||||||
| 	policy.Spec.AuditAnnotations = auditAnnotations | 	policy.Spec.AuditAnnotations = auditAnnotations | ||||||
| 	return policy | 	return policy | ||||||
| @@ -2995,7 +3222,7 @@ func checkExpectedError(t *testing.T, err error, expectedErr string) { | |||||||
| 		t.Fatal("got error but expected none") | 		t.Fatal("got error but expected none") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err.Error() != expectedErr { | 	if !strings.Contains(err.Error(), expectedErr) { | ||||||
| 		t.Logf("actual validation error: %v", err) | 		t.Logf("actual validation error: %v", err) | ||||||
| 		t.Logf("expected validation error: %v", expectedErr) | 		t.Logf("expected validation error: %v", expectedErr) | ||||||
| 		t.Error("unexpected validation error") | 		t.Error("unexpected validation error") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot