2023-06-01 00:30:55 +03:00
package validation
import (
"context"
"fmt"
2024-07-25 20:36:19 +03:00
"strings"
2023-06-01 00:30:55 +03:00
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
2024-06-24 23:36:55 +07:00
kyvernov2 "github.com/kyverno/kyverno/api/kyverno/v2"
2023-06-01 00:30:55 +03:00
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/handlers"
2023-09-05 13:16:50 +03:00
"github.com/kyverno/kyverno/pkg/engine/internal"
2023-06-01 00:30:55 +03:00
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
2023-08-24 17:06:37 +03:00
celutils "github.com/kyverno/kyverno/pkg/utils/cel"
2024-04-22 21:49:25 +08:00
datautils "github.com/kyverno/kyverno/pkg/utils/data"
2024-02-02 16:32:28 +08:00
vaputils "github.com/kyverno/kyverno/pkg/validatingadmissionpolicy"
2024-08-29 18:31:25 +03:00
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
2023-08-21 11:04:59 +03:00
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2023-06-01 00:30:55 +03:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
2024-06-04 15:09:44 +08:00
"k8s.io/apiserver/pkg/admission/plugin/policy/validating"
2023-06-01 00:30:55 +03:00
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
2023-11-13 17:43:25 +02:00
"k8s.io/client-go/tools/cache"
2023-06-01 00:30:55 +03:00
)
type validateCELHandler struct {
2024-12-19 09:42:54 +02:00
client engineapi . Client
isCluster bool
2023-06-01 00:30:55 +03:00
}
2024-12-19 09:42:54 +02:00
func NewValidateCELHandler ( client engineapi . Client , isCluster bool ) ( handlers . Handler , error ) {
2023-06-01 00:30:55 +03:00
return validateCELHandler {
2024-12-19 09:42:54 +02:00
client : client ,
isCluster : isCluster ,
2023-06-01 00:30:55 +03:00
} , nil
}
func ( h validateCELHandler ) Process (
ctx context . Context ,
logger logr . Logger ,
policyContext engineapi . PolicyContext ,
resource unstructured . Unstructured ,
rule kyvernov1 . Rule ,
_ engineapi . EngineContextLoader ,
2024-06-24 23:36:55 +07:00
exceptions [ ] * kyvernov2 . PolicyException ,
2023-06-01 00:30:55 +03:00
) ( unstructured . Unstructured , [ ] engineapi . RuleResponse ) {
2024-07-25 20:36:19 +03:00
// check if there are policy exceptions that match the incoming resource
matchedExceptions := engineutils . MatchesException ( exceptions , policyContext , logger )
if len ( matchedExceptions ) > 0 {
var keys [ ] string
for i , exception := range matchedExceptions {
key , err := cache . MetaNamespaceKeyFunc ( & matchedExceptions [ i ] )
if err != nil {
logger . Error ( err , "failed to compute policy exception key" , "namespace" , exception . GetNamespace ( ) , "name" , exception . GetName ( ) )
return resource , handlers . WithError ( rule , engineapi . Validation , "failed to compute exception key" , err )
}
keys = append ( keys , key )
2023-11-13 17:43:25 +02:00
}
2024-07-25 20:36:19 +03:00
logger . V ( 3 ) . Info ( "policy rule is skipped due to policy exceptions" , "exceptions" , keys )
return resource , handlers . WithResponses (
2024-09-03 23:06:07 +05:30
engineapi . RuleSkip ( rule . Name , engineapi . Validation , "rule is skipped due to policy exceptions" + strings . Join ( keys , ", " ) , rule . ReportProperties ) . WithExceptions ( matchedExceptions ) ,
2024-07-25 20:36:19 +03:00
)
2023-11-13 17:43:25 +02:00
}
2023-08-31 13:25:21 +03:00
// check if a corresponding validating admission policy is generated
vapStatus := policyContext . Policy ( ) . GetStatus ( ) . ValidatingAdmissionPolicy
if vapStatus . Generated {
2023-11-13 16:53:24 +02:00
logger . V ( 3 ) . Info ( "skipping CEL validation due to the generation of its corresponding ValidatingAdmissionPolicy" )
2023-08-31 13:25:21 +03:00
return resource , nil
}
2023-06-01 00:30:55 +03:00
2023-08-23 16:28:40 +03:00
// get resource's name, namespace, GroupVersionResource, and GroupVersionKind
2023-08-18 05:54:05 +02:00
gvr := schema . GroupVersionResource ( policyContext . RequestResource ( ) )
2024-07-05 00:46:36 +08:00
gvk , _ := policyContext . ResourceKind ( )
2023-09-05 13:16:50 +03:00
policyKind := policyContext . Policy ( ) . GetKind ( )
policyName := policyContext . Policy ( ) . GetName ( )
2024-07-05 00:46:36 +08:00
// in case of UPDATE requests, set the oldObject to the current resource before it gets updated
var object , oldObject runtime . Object
2023-08-21 11:04:59 +03:00
oldResource := policyContext . OldResource ( )
2023-06-01 00:30:55 +03:00
if oldResource . Object == nil {
oldObject = nil
} else {
oldObject = oldResource . DeepCopyObject ( )
}
2024-07-05 00:46:36 +08:00
var ns , name string
// in case of DELETE request, get the name and the namespace from the old object
if resource . Object == nil {
ns = oldResource . GetNamespace ( )
name = oldResource . GetName ( )
object = nil
} else {
ns = resource . GetNamespace ( )
name = resource . GetName ( )
object = resource . DeepCopyObject ( )
}
2023-08-23 16:28:40 +03:00
// check if the rule uses parameter resources
hasParam := rule . Validation . CEL . HasParam ( )
// extract preconditions written as CEL expressions
matchConditions := rule . CELPreconditions
// extract CEL expressions used in validations and audit annotations
2023-08-24 13:00:27 +03:00
variables := rule . Validation . CEL . Variables
2023-06-01 00:30:55 +03:00
validations := rule . Validation . CEL . Expressions
2023-11-13 16:53:24 +02:00
for i := range validations {
if validations [ i ] . Message == "" {
validations [ i ] . Message = rule . Validation . Message
}
}
2023-06-01 00:30:55 +03:00
auditAnnotations := rule . Validation . CEL . AuditAnnotations
2023-09-05 13:16:50 +03:00
optionalVars := cel . OptionalVariableDeclarations { HasParams : hasParam , HasAuthorizer : true }
expressionOptionalVars := cel . OptionalVariableDeclarations { HasParams : hasParam , HasAuthorizer : false }
2023-08-21 11:04:59 +03:00
// compile CEL expressions
2024-02-02 16:32:28 +08:00
compiler , err := celutils . NewCompiler ( validations , auditAnnotations , vaputils . ConvertMatchConditionsV1 ( matchConditions ) , variables )
2023-08-18 05:54:05 +02:00
if err != nil {
return resource , handlers . WithError ( rule , engineapi . Validation , "Error while creating composited compiler" , err )
}
2023-08-24 17:06:37 +03:00
compiler . CompileVariables ( optionalVars )
filter := compiler . CompileValidateExpressions ( optionalVars )
2023-09-05 13:16:50 +03:00
messageExpressionfilter := compiler . CompileMessageExpressions ( expressionOptionalVars )
2023-08-24 17:06:37 +03:00
auditAnnotationFilter := compiler . CompileAuditAnnotationsExpressions ( optionalVars )
matchConditionFilter := compiler . CompileMatchExpressions ( optionalVars )
2023-06-01 00:30:55 +03:00
2023-08-21 11:04:59 +03:00
// newMatcher will be used to check if the incoming resource matches the CEL preconditions
2023-09-05 13:16:50 +03:00
newMatcher := matchconditions . NewMatcher ( matchConditionFilter , nil , policyKind , "" , policyName )
2023-08-23 16:28:40 +03:00
// newValidator will be used to validate CEL expressions against the incoming object
2024-06-04 15:09:44 +08:00
validator := validating . NewValidator ( filter , newMatcher , auditAnnotationFilter , messageExpressionfilter , nil )
2023-08-21 11:04:59 +03:00
var namespace * corev1 . Namespace
// Special case, the namespace object has the namespace of itself.
// unset it if the incoming object is a namespace
if gvk . Kind == "Namespace" && gvk . Version == "v1" && gvk . Group == "" {
2024-07-05 00:46:36 +08:00
ns = ""
2023-08-21 11:04:59 +03:00
}
2024-07-05 00:46:36 +08:00
if ns != "" {
2024-12-19 09:42:54 +02:00
if h . client != nil && h . isCluster {
2024-07-05 00:46:36 +08:00
namespace , err = h . client . GetNamespace ( ctx , ns , metav1 . GetOptions { } )
2024-04-19 18:55:41 +08:00
if err != nil {
return resource , handlers . WithResponses (
2024-09-03 23:06:07 +05:30
engineapi . RuleError ( rule . Name , engineapi . Validation , "Error getting the resource's namespace" , err , rule . ReportProperties ) ,
2024-04-19 18:55:41 +08:00
)
}
} else {
namespace = & corev1 . Namespace {
ObjectMeta : metav1 . ObjectMeta {
2024-07-05 00:46:36 +08:00
Name : ns ,
2024-04-19 18:55:41 +08:00
} ,
}
2023-08-21 11:04:59 +03:00
}
}
2023-09-05 13:16:50 +03:00
requestInfo := policyContext . AdmissionInfo ( )
userInfo := internal . NewUser ( requestInfo . AdmissionUserInfo . Username , requestInfo . AdmissionUserInfo . UID , requestInfo . AdmissionUserInfo . Groups )
2024-07-05 00:46:36 +08:00
attr := admission . NewAttributesRecord ( object , oldObject , gvk , ns , name , gvr , "" , admission . Operation ( policyContext . Operation ( ) ) , nil , false , & userInfo )
o := admission . NewObjectInterfacesFromScheme ( runtime . NewScheme ( ) )
versionedAttr , err := admission . NewVersionedAttributes ( attr , attr . GetKind ( ) , o )
if err != nil {
return resource , handlers . WithError ( rule , engineapi . Validation , "error while creating versioned attributes" , err )
}
authorizer := internal . NewAuthorizer ( h . client , gvk )
2023-08-21 11:04:59 +03:00
// validate the incoming object against the rule
2024-06-04 15:09:44 +08:00
var validationResults [ ] validating . ValidateResult
2023-08-28 17:43:09 +03:00
if hasParam {
paramKind := rule . Validation . CEL . ParamKind
paramRef := rule . Validation . CEL . ParamRef
2023-06-01 00:30:55 +03:00
2024-07-05 00:46:36 +08:00
params , err := collectParams ( ctx , h . client , paramKind , paramRef , ns )
2023-08-28 17:43:09 +03:00
if err != nil {
return resource , handlers . WithResponses (
2024-09-03 23:06:07 +05:30
engineapi . RuleError ( rule . Name , engineapi . Validation , "error in parameterized resource" , err , rule . ReportProperties ) ,
2023-08-28 17:43:09 +03:00
)
}
for _ , param := range params {
2023-09-05 13:16:50 +03:00
validationResults = append ( validationResults , validator . Validate ( ctx , gvr , versionedAttr , param , namespace , celconfig . RuntimeCELCostBudget , & authorizer ) )
2023-08-28 17:43:09 +03:00
}
} else {
2023-09-05 13:16:50 +03:00
validationResults = append ( validationResults , validator . Validate ( ctx , gvr , versionedAttr , nil , namespace , celconfig . RuntimeCELCostBudget , & authorizer ) )
2023-08-28 17:43:09 +03:00
}
for _ , validationResult := range validationResults {
2024-04-22 21:49:25 +08:00
// no validations are returned if preconditions aren't met
2024-06-04 15:09:44 +08:00
if datautils . DeepEqual ( validationResult , validating . ValidateResult { } ) {
2024-04-22 21:49:25 +08:00
return resource , handlers . WithResponses (
2024-09-03 23:06:07 +05:30
engineapi . RuleSkip ( rule . Name , engineapi . Validation , "cel preconditions not met" , rule . ReportProperties ) ,
2024-04-22 21:49:25 +08:00
)
}
2023-08-28 17:43:09 +03:00
for _ , decision := range validationResult . Decisions {
switch decision . Action {
2024-06-04 15:09:44 +08:00
case validating . ActionAdmit :
if decision . Evaluation == validating . EvalError {
2023-08-28 17:43:09 +03:00
return resource , handlers . WithResponses (
2024-09-03 23:06:07 +05:30
engineapi . RuleError ( rule . Name , engineapi . Validation , decision . Message , nil , rule . ReportProperties ) ,
2023-08-28 17:43:09 +03:00
)
}
2024-06-04 15:09:44 +08:00
case validating . ActionDeny :
2023-06-01 00:30:55 +03:00
return resource , handlers . WithResponses (
2024-09-03 23:06:07 +05:30
engineapi . RuleFail ( rule . Name , engineapi . Validation , decision . Message , rule . ReportProperties ) ,
2023-06-01 00:30:55 +03:00
)
}
}
}
msg := fmt . Sprintf ( "Validation rule '%s' passed." , rule . Name )
return resource , handlers . WithResponses (
2024-09-03 23:06:07 +05:30
engineapi . RulePass ( rule . Name , engineapi . Validation , msg , rule . ReportProperties ) ,
2023-06-01 00:30:55 +03:00
)
}
2023-08-28 17:43:09 +03:00
2024-08-29 18:31:25 +03:00
func collectParams ( ctx context . Context , client engineapi . Client , paramKind * admissionregistrationv1beta1 . ParamKind , paramRef * admissionregistrationv1beta1 . ParamRef , namespace string ) ( [ ] runtime . Object , error ) {
2023-08-28 17:43:09 +03:00
var params [ ] runtime . Object
apiVersion := paramKind . APIVersion
kind := paramKind . Kind
gv , err := schema . ParseGroupVersion ( apiVersion )
if err != nil {
return nil , fmt . Errorf ( "can't parse the parameter resource group version" )
}
// If `paramKind` is cluster-scoped, then paramRef.namespace MUST be unset.
// If `paramKind` is namespace-scoped, the namespace of the object being evaluated for admission will be used
// when paramRef.namespace is left unset.
var paramsNamespace string
isNamespaced , err := client . IsNamespaced ( gv . Group , gv . Version , kind )
if err != nil {
2023-09-08 15:51:25 +02:00
return nil , fmt . Errorf ( "failed to check if resource is namespaced or not (%w)" , err )
2023-08-28 17:43:09 +03:00
}
// check if `paramKind` is namespace-scoped
if isNamespaced {
// set params namespace to the incoming object's namespace by default.
paramsNamespace = namespace
if paramRef . Namespace != "" {
paramsNamespace = paramRef . Namespace
} else if paramsNamespace == "" {
return nil , fmt . Errorf ( "can't use namespaced paramRef to match cluster-scoped resources" )
}
} else {
// It isn't allowed to set namespace for cluster-scoped params
if paramRef . Namespace != "" {
return nil , fmt . Errorf ( "paramRef.namespace must not be provided for a cluster-scoped `paramKind`" )
}
}
if paramRef . Name != "" {
param , err := client . GetResource ( ctx , apiVersion , kind , paramsNamespace , paramRef . Name , "" )
if err != nil {
return nil , err
}
return [ ] runtime . Object { param } , nil
} else if paramRef . Selector != nil {
paramList , err := client . ListResource ( ctx , apiVersion , kind , paramsNamespace , paramRef . Selector )
if err != nil {
return nil , err
}
for i := range paramList . Items {
params = append ( params , & paramList . Items [ i ] )
}
}
2024-08-29 18:31:25 +03:00
if len ( params ) == 0 && paramRef . ParameterNotFoundAction != nil && * paramRef . ParameterNotFoundAction == admissionregistrationv1beta1 . DenyAction {
2023-08-28 17:43:09 +03:00
return nil , fmt . Errorf ( "no params found" )
}
return params , nil
}