package engine import ( "context" "strings" 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" celpolicy "github.com/kyverno/kyverno/pkg/cel/policy" engineapi "github.com/kyverno/kyverno/pkg/engine/api" "github.com/kyverno/kyverno/pkg/engine/handlers" admissionutils "github.com/kyverno/kyverno/pkg/utils/admission" admissionv1 "k8s.io/api/admission/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" authenticationv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/admission" "k8s.io/client-go/tools/cache" "k8s.io/utils/ptr" ) type EngineRequest struct { jsonPayload *unstructured.Unstructured request admissionv1.AdmissionRequest context contextlib.ContextInterface } func RequestFromAdmission(context contextlib.ContextInterface, request admissionv1.AdmissionRequest) EngineRequest { return EngineRequest{ request: request, context: context, } } func RequestFromJSON(context contextlib.ContextInterface, jsonPayload *unstructured.Unstructured) EngineRequest { return EngineRequest{ jsonPayload: jsonPayload, context: context, } } func Request( context contextlib.ContextInterface, gvk schema.GroupVersionKind, gvr schema.GroupVersionResource, subResource string, name string, namespace string, operation admissionv1.Operation, userInfo authenticationv1.UserInfo, object runtime.Object, oldObject runtime.Object, dryRun bool, options runtime.Object, ) EngineRequest { request := admissionv1.AdmissionRequest{ Kind: metav1.GroupVersionKind(gvk), Resource: metav1.GroupVersionResource(gvr), SubResource: subResource, RequestKind: ptr.To(metav1.GroupVersionKind(gvk)), RequestResource: ptr.To(metav1.GroupVersionResource(gvr)), RequestSubResource: subResource, Name: name, Namespace: namespace, Operation: operation, UserInfo: userInfo, Object: runtime.RawExtension{Object: object}, OldObject: runtime.RawExtension{Object: oldObject}, DryRun: &dryRun, Options: runtime.RawExtension{Object: options}, } return RequestFromAdmission(context, request) } func (r *EngineRequest) AdmissionRequest() admissionv1.AdmissionRequest { return r.request } type EngineResponse struct { Resource *unstructured.Unstructured Policies []ValidatingPolicyResponse } type ValidatingPolicyResponse struct { Actions sets.Set[admissionregistrationv1.ValidationAction] Policy policiesv1alpha1.ValidatingPolicy Rules []engineapi.RuleResponse } type Engine interface { Handle(context.Context, EngineRequest) (EngineResponse, error) } type NamespaceResolver = func(string) *corev1.Namespace type engine struct { provider VPolProviderFunc nsResolver NamespaceResolver matcher matching.Matcher } func NewEngine(provider VPolProviderFunc, nsResolver NamespaceResolver, matcher matching.Matcher) Engine { return &engine{ provider: provider, nsResolver: nsResolver, matcher: matcher, } } func (e *engine) Handle(ctx context.Context, request EngineRequest) (EngineResponse, error) { var response EngineResponse // fetch compiled policies policies, err := e.provider.CompiledValidationPolicies(ctx) if err != nil { return response, err } if request.jsonPayload != nil { response.Resource = request.jsonPayload for _, policy := range policies { response.Policies = append(response.Policies, e.handlePolicy(ctx, policy, request.jsonPayload.Object, nil, nil, nil, request.context)) } return response, nil } // load objects object, oldObject, err := admissionutils.ExtractResources(nil, request.request) if err != nil { return response, err } response.Resource = &object if response.Resource.Object == nil { response.Resource = &oldObject } // default dry run dryRun := false if request.request.DryRun != nil { dryRun = *request.request.DryRun } // create admission attributes attr := admission.NewAttributesRecord( &object, &oldObject, schema.GroupVersionKind(request.request.Kind), request.request.Namespace, request.request.Name, schema.GroupVersionResource(request.request.Resource), request.request.SubResource, admission.Operation(request.request.Operation), nil, dryRun, // TODO nil, ) // resolve namespace var namespace runtime.Object if ns := request.request.Namespace; ns != "" { namespace = e.nsResolver(ns) } // evaluate policies for _, policy := range policies { response.Policies = append(response.Policies, e.handlePolicy(ctx, policy, nil, attr, &request.request, namespace, request.context)) } return response, nil } func (e *engine) matchPolicy(policy CompiledValidatingPolicy, 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 if policy.Policy.GetSpec().MatchConstraints != nil { 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 CompiledValidatingPolicy, jsonPayload interface{}, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface) ValidatingPolicyResponse { response := ValidatingPolicyResponse{ Actions: policy.Actions, Policy: policy.Policy, } autogenIndex := -1 if e.matcher != 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 } var result *celpolicy.EvaluationResult var err error if jsonPayload != nil { result, err = policy.CompiledPolicy.Evaluate(ctx, jsonPayload, nil, nil, nil, context, -1) } else { result, err = policy.CompiledPolicy.Evaluate(ctx, nil, 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)) } else if len(result.Exceptions) > 0 { exceptions := make([]engineapi.GenericException, 0, len(result.Exceptions)) var keys []string for i := range result.Exceptions { key, err := cache.MetaNamespaceKeyFunc(&result.Exceptions[i]) if err != nil { response.Rules = handlers.WithResponses(engineapi.RuleError("exception", engineapi.Validation, "failed to compute exception key", err, nil)) return response } keys = append(keys, key) exceptions = append(exceptions, engineapi.NewCELPolicyException(&result.Exceptions[i])) } response.Rules = handlers.WithResponses(engineapi.RuleSkip("exception", engineapi.Validation, "rule is skipped due to policy exception: "+strings.Join(keys, ", "), nil).WithExceptions(exceptions)) } else { // TODO: do we want to set a rule name? ruleName := "" if result.Error != nil { response.Rules = append(response.Rules, *engineapi.RuleError(ruleName, engineapi.Validation, "error", err, nil)) } else if result.Result { response.Rules = append(response.Rules, *engineapi.RulePass(ruleName, engineapi.Validation, "success", nil)) } else { response.Rules = append(response.Rules, *engineapi.RuleFail(ruleName, engineapi.Validation, result.Message, result.AuditAnnotations)) } } return response }