From 86fff3b3941f0c96611673c48544cb8b920f2ab0 Mon Sep 17 00:00:00 2001 From: Mariam Fahmy Date: Sat, 15 Feb 2025 06:56:51 +0200 Subject: [PATCH] feat: compile and evaluate autogen rules (#12163) --- pkg/cel/engine/engine.go | 42 ++++++++- pkg/cel/policy/compiler.go | 155 +++++++++++++++++++++++--------- pkg/cel/policy/compiler_test.go | 34 +++++++ pkg/cel/policy/policy.go | 41 ++++++--- 4 files changed, 215 insertions(+), 57 deletions(-) diff --git a/pkg/cel/engine/engine.go b/pkg/cel/engine/engine.go index 487cf7b999..e672863f70 100644 --- a/pkg/cel/engine/engine.go +++ b/pkg/cel/engine/engine.go @@ -5,6 +5,7 @@ import ( "fmt" policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1" + vpolautogen "github.com/kyverno/kyverno/pkg/cel/autogen" contextlib "github.com/kyverno/kyverno/pkg/cel/libs/context" "github.com/kyverno/kyverno/pkg/cel/matching" "github.com/kyverno/kyverno/pkg/cel/utils" @@ -151,21 +152,56 @@ func (e *engine) Handle(ctx context.Context, request EngineRequest) (EngineRespo return response, nil } +func (e *engine) matchPolicy(policy CompiledPolicy, attr admission.Attributes, namespace runtime.Object) (bool, int, error) { + match := func(constraints *admissionregistrationv1.MatchResources) (bool, error) { + criteria := matchCriteria{constraints: constraints} + matches, err := e.matcher.Match(&criteria, attr, namespace) + if err != nil { + return false, err + } + return matches, nil + } + + // match against main policy constraints + matches, err := match(policy.Policy.Spec.MatchConstraints) + if err != nil { + return false, -1, err + } + if matches { + return true, -1, nil + } + + // match against autogen rules + autogenRules := vpolautogen.ComputeRules(&policy.Policy) + for i, autogenRule := range autogenRules { + matches, err := match(autogenRule.MatchConstraints) + if err != nil { + return false, -1, err + } + if matches { + return true, i, nil + } + } + return false, -1, nil +} + func (e *engine) handlePolicy(ctx context.Context, policy CompiledPolicy, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface) PolicyResponse { response := PolicyResponse{ Actions: policy.Actions, Policy: policy.Policy, } + autogenIndex := -1 if e.matcher != nil { - criteria := matchCriteria{constraints: policy.Policy.Spec.MatchConstraints} - if matches, err := e.matcher.Match(&criteria, attr, namespace); err != nil { + matches, index, err := e.matchPolicy(policy, attr, namespace) + if err != nil { response.Rules = handlers.WithResponses(engineapi.RuleError("match", engineapi.Validation, "failed to execute matching", err, nil)) return response } else if !matches { return response } + autogenIndex = index } - results, err := policy.CompiledPolicy.Evaluate(ctx, attr, request, namespace, context) + results, err := policy.CompiledPolicy.Evaluate(ctx, attr, request, namespace, context, autogenIndex) // TODO: error is about match conditions here ? if err != nil { response.Rules = handlers.WithResponses(engineapi.RuleError("evaluation", engineapi.Validation, "failed to load context", err, nil)) diff --git a/pkg/cel/policy/compiler.go b/pkg/cel/policy/compiler.go index 2536aa9442..42b109bdba 100644 --- a/pkg/cel/policy/compiler.go +++ b/pkg/cel/policy/compiler.go @@ -7,6 +7,7 @@ import ( "github.com/google/cel-go/common/types" policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1" engine "github.com/kyverno/kyverno/pkg/cel" + vpolautogen "github.com/kyverno/kyverno/pkg/cel/autogen" "github.com/kyverno/kyverno/pkg/cel/libs/context" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" "k8s.io/apimachinery/pkg/util/validation/field" @@ -70,38 +71,18 @@ func (c *compiler) Compile(policy *policiesv1alpha1.ValidatingPolicy, exceptions matchConditions := make([]cel.Program, 0, len(policy.Spec.MatchConditions)) { path := path.Child("matchConditions") - for i, matchCondition := range policy.Spec.MatchConditions { - path := path.Index(i).Child("expression") - ast, issues := env.Compile(matchCondition.Expression) - if err := issues.Err(); err != nil { - return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error())) - } - if !ast.OutputType().IsExactType(types.BoolType) { - msg := fmt.Sprintf("output is expected to be of type %s", types.BoolType.TypeName()) - return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, msg)) - } - prog, err := env.Program(ast) - if err != nil { - return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error())) - } - matchConditions = append(matchConditions, prog) + programs, errs := compileMatchConditions(path, policy.Spec.MatchConditions, env) + if errs != nil { + return nil, append(allErrs, errs...) } + matchConditions = append(matchConditions, programs...) } variables := map[string]cel.Program{} { path := path.Child("variables") - for i, variable := range policy.Spec.Variables { - path := path.Index(i).Child("expression") - ast, issues := env.Compile(variable.Expression) - if err := issues.Err(); err != nil { - return nil, append(allErrs, field.Invalid(path, variable.Expression, err.Error())) - } - variablesProvider.RegisterField(variable.Name, ast.OutputType()) - prog, err := env.Program(ast) - if err != nil { - return nil, append(allErrs, field.Invalid(path, variable.Expression, err.Error())) - } - variables[variable.Name] = prog + errs := compileVariables(path, policy.Spec.Variables, variablesProvider, env, variables) + if errs != nil { + return nil, append(allErrs, errs...) } } validations := make([]compiledValidation, 0, len(policy.Spec.Validations)) @@ -119,24 +100,52 @@ func (c *compiler) Compile(policy *policiesv1alpha1.ValidatingPolicy, exceptions auditAnnotations := map[string]cel.Program{} { path := path.Child("auditAnnotations") - for i, auditAnnotation := range policy.Spec.AuditAnnotations { - path := path.Index(i).Child("valueExpression") - ast, issues := env.Compile(auditAnnotation.ValueExpression) - if err := issues.Err(); err != nil { - return nil, append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, err.Error())) - } - if !ast.OutputType().IsExactType(types.StringType) && !ast.OutputType().IsExactType(types.NullType) { - msg := fmt.Sprintf("output is expected to be either of type %s or %s", types.StringType.TypeName(), types.NullType.TypeName()) - return nil, append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, msg)) - } - prog, err := env.Program(ast) - if err != nil { - return nil, append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, err.Error())) - } - auditAnnotations[auditAnnotation.Key] = prog + errs := compileAuditAnnotations(path, policy.Spec.AuditAnnotations, env, auditAnnotations) + if errs != nil { + return nil, append(allErrs, errs...) } } + // compile autogen rules + autogenPath := field.NewPath("status").Child("autogen").Child("rules") + autogenRules := vpolautogen.ComputeRules(policy) + compiledRules := make([]compiledAutogenRule, 0, len(autogenRules)) + for i, rule := range autogenRules { + // compile match conditions + matchConditions, errs := compileMatchConditions(autogenPath.Index(i).Child("matchConditions"), rule.MatchConditions, env) + if errs != nil { + return nil, append(allErrs, errs...) + } + // compile variables + variables := map[string]cel.Program{} + errs = compileVariables(autogenPath.Index(i).Child("variables"), rule.Variables, variablesProvider, env, variables) + if errs != nil { + return nil, append(allErrs, errs...) + } + // compile validations + validations := make([]compiledValidation, 0, len(rule.Validations)) + for j, rule := range rule.Validations { + path := autogenPath.Index(j).Child("validations") + program, errs := compileValidation(path, rule, env) + if errs != nil { + return nil, append(allErrs, errs...) + } + validations = append(validations, program) + } + // compile audit annotations + auditAnnotations := map[string]cel.Program{} + errs = compileAuditAnnotations(autogenPath.Index(i).Child("auditAnnotations"), rule.AuditAnnotation, env, auditAnnotations) + if errs != nil { + return nil, append(allErrs, errs...) + } + compiledRules = append(compiledRules, compiledAutogenRule{ + matchConditions: matchConditions, + variables: variables, + validations: validations, + auditAnnotation: auditAnnotations, + }) + } + // exceptions' match conditions var polexMatchConditions []cel.Program if len(exceptions) > 0 { @@ -168,9 +177,71 @@ func (c *compiler) Compile(policy *policiesv1alpha1.ValidatingPolicy, exceptions validations: validations, auditAnnotations: auditAnnotations, polexMatchConditions: polexMatchConditions, + autogenRules: compiledRules, }, nil } +func compileMatchConditions(path *field.Path, matchConditions []admissionregistrationv1.MatchCondition, env *cel.Env) ([]cel.Program, field.ErrorList) { + var allErrs field.ErrorList + result := make([]cel.Program, 0, len(matchConditions)) + for i, matchCondition := range matchConditions { + path := path.Index(i).Child("expression") + ast, issues := env.Compile(matchCondition.Expression) + if err := issues.Err(); err != nil { + return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error())) + } + if !ast.OutputType().IsExactType(types.BoolType) { + msg := fmt.Sprintf("output is expected to be of type %s", types.BoolType.TypeName()) + return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, msg)) + } + prog, err := env.Program(ast) + if err != nil { + return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error())) + } + result = append(result, prog) + } + return result, nil +} + +func compileVariables(path *field.Path, variables []admissionregistrationv1.Variable, variablesProvider *variablesProvider, env *cel.Env, result map[string]cel.Program) field.ErrorList { + var allErrs field.ErrorList + for i, variable := range variables { + path := path.Index(i).Child("expression") + ast, issues := env.Compile(variable.Expression) + if err := issues.Err(); err != nil { + return append(allErrs, field.Invalid(path, variable.Expression, err.Error())) + } + variablesProvider.RegisterField(variable.Name, ast.OutputType()) + prog, err := env.Program(ast) + if err != nil { + return append(allErrs, field.Invalid(path, variable.Expression, err.Error())) + } + result[variable.Name] = prog + } + return nil +} + +func compileAuditAnnotations(path *field.Path, auditAnnotations []admissionregistrationv1.AuditAnnotation, env *cel.Env, result map[string]cel.Program) field.ErrorList { + var allErrs field.ErrorList + for i, auditAnnotation := range auditAnnotations { + path := path.Index(i).Child("valueExpression") + ast, issues := env.Compile(auditAnnotation.ValueExpression) + if err := issues.Err(); err != nil { + return append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, err.Error())) + } + if !ast.OutputType().IsExactType(types.StringType) && !ast.OutputType().IsExactType(types.NullType) { + msg := fmt.Sprintf("output is expected to be either of type %s or %s", types.StringType.TypeName(), types.NullType.TypeName()) + return append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, msg)) + } + prog, err := env.Program(ast) + if err != nil { + return append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, err.Error())) + } + result[auditAnnotation.Key] = prog + } + return nil +} + func compileValidation(path *field.Path, rule admissionregistrationv1.Validation, env *cel.Env) (compiledValidation, field.ErrorList) { var allErrs field.ErrorList compiled := compiledValidation{ diff --git a/pkg/cel/policy/compiler_test.go b/pkg/cel/policy/compiler_test.go index be6b98e1c0..b07b2e60f3 100644 --- a/pkg/cel/policy/compiler_test.go +++ b/pkg/cel/policy/compiler_test.go @@ -26,6 +26,23 @@ func Test_compiler_Compile(t *testing.T) { }, Spec: policiesv1alpha1.ValidatingPolicySpec{ ValidatingAdmissionPolicySpec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistrationv1.MatchResources{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + admissionregistrationv1.Update, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"pods"}, + }, + }, + }, + }, + }, Variables: []admissionregistrationv1.Variable{{ Name: "environment", Expression: "has(object.metadata.labels) && 'env' in object.metadata.labels && object.metadata.labels['env'] == 'prod'", @@ -48,6 +65,23 @@ func Test_compiler_Compile(t *testing.T) { }, Spec: policiesv1alpha1.ValidatingPolicySpec{ ValidatingAdmissionPolicySpec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistrationv1.MatchResources{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ + { + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + admissionregistrationv1.Update, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"pods"}, + }, + }, + }, + }, + }, Variables: []admissionregistrationv1.Variable{{ Name: "cm", Expression: "context.GetConfigMap('foo', 'bar')", diff --git a/pkg/cel/policy/policy.go b/pkg/cel/policy/policy.go index 32e6e7f166..ec12be0b40 100644 --- a/pkg/cel/policy/policy.go +++ b/pkg/cel/policy/policy.go @@ -26,7 +26,7 @@ type EvaluationResult struct { } type CompiledPolicy interface { - Evaluate(context.Context, admission.Attributes, *admissionv1.AdmissionRequest, runtime.Object, contextlib.ContextInterface) ([]EvaluationResult, error) + Evaluate(context.Context, admission.Attributes, *admissionv1.AdmissionRequest, runtime.Object, contextlib.ContextInterface, int) ([]EvaluationResult, error) } type compiledValidation struct { @@ -35,6 +35,13 @@ type compiledValidation struct { program cel.Program } +type compiledAutogenRule struct { + matchConditions []cel.Program + validations []compiledValidation + auditAnnotation map[string]cel.Program + variables map[string]cel.Program +} + type compiledPolicy struct { failurePolicy admissionregistrationv1.FailurePolicyType matchConditions []cel.Program @@ -42,6 +49,7 @@ type compiledPolicy struct { validations []compiledValidation auditAnnotations map[string]cel.Program polexMatchConditions []cel.Program + autogenRules []compiledAutogenRule } func (p *compiledPolicy) Evaluate( @@ -50,10 +58,11 @@ func (p *compiledPolicy) Evaluate( request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface, + autogenIndex int, ) ([]EvaluationResult, error) { // check if the resource matches an exception if len(p.polexMatchConditions) > 0 { - match, err := p.match(ctx, attr, request, namespace, true) + match, err := p.match(ctx, attr, request, namespace, p.polexMatchConditions) if err != nil { return nil, err } @@ -62,7 +71,20 @@ func (p *compiledPolicy) Evaluate( } } - match, err := p.match(ctx, attr, request, namespace, false) + var matchConditions []cel.Program + var validations []compiledValidation + var variables map[string]cel.Program + + if autogenIndex != -1 { + matchConditions = p.autogenRules[autogenIndex].matchConditions + validations = p.autogenRules[autogenIndex].validations + variables = p.autogenRules[autogenIndex].variables + } else { + matchConditions = p.matchConditions + validations = p.validations + variables = p.variables + } + match, err := p.match(ctx, attr, request, namespace, matchConditions) if err != nil { return nil, err } @@ -94,7 +116,7 @@ func (p *compiledPolicy) Evaluate( RequestKey: requestVal.Object, VariablesKey: vars, } - for name, variable := range p.variables { + for name, variable := range variables { vars.Append(name, func(*lazy.MapValue) ref.Val { out, _, err := variable.ContextEval(ctx, data) if out != nil { @@ -106,8 +128,8 @@ func (p *compiledPolicy) Evaluate( return nil }) } - results := make([]EvaluationResult, 0, len(p.validations)) - for _, validation := range p.validations { + results := make([]EvaluationResult, 0, len(validations)) + for _, validation := range validations { out, _, err := validation.program.ContextEval(ctx, data) // evaluate only when rule fails var message string @@ -137,7 +159,7 @@ func (p *compiledPolicy) match( attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, - isException bool, + matchConditions []cel.Program, ) (bool, error) { namespaceVal, err := objectToResolveVal(namespace) if err != nil { @@ -162,11 +184,6 @@ func (p *compiledPolicy) match( RequestKey: requestVal.Object, } var errs []error - - matchConditions := p.matchConditions - if isException { - matchConditions = p.polexMatchConditions - } for _, matchCondition := range matchConditions { // evaluate the condition out, _, err := matchCondition.ContextEval(ctx, data)