k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/validating/plugin.go (about)

     1  /*
     2  Copyright 2024 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package validating
    18  
    19  import (
    20  	"context"
    21  	"io"
    22  	"sync"
    23  
    24  	v1 "k8s.io/api/admissionregistration/v1"
    25  	"k8s.io/apimachinery/pkg/api/meta"
    26  	"k8s.io/apiserver/pkg/admission"
    27  	"k8s.io/apiserver/pkg/admission/initializer"
    28  	"k8s.io/apiserver/pkg/admission/plugin/cel"
    29  	"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
    30  	"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
    31  	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
    32  	"k8s.io/apiserver/pkg/authorization/authorizer"
    33  	"k8s.io/apiserver/pkg/cel/environment"
    34  	"k8s.io/apiserver/pkg/features"
    35  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    36  	"k8s.io/client-go/dynamic"
    37  	"k8s.io/client-go/informers"
    38  	"k8s.io/client-go/kubernetes"
    39  	"k8s.io/component-base/featuregate"
    40  )
    41  
    42  const (
    43  	// PluginName indicates the name of admission plug-in
    44  	PluginName = "ValidatingAdmissionPolicy"
    45  )
    46  
    47  var (
    48  	lazyCompositionEnvTemplateWithStrictCostInit sync.Once
    49  	lazyCompositionEnvTemplateWithStrictCost     *cel.CompositionEnv
    50  
    51  	lazyCompositionEnvTemplateWithoutStrictCostInit sync.Once
    52  	lazyCompositionEnvTemplateWithoutStrictCost     *cel.CompositionEnv
    53  )
    54  
    55  func getCompositionEnvTemplateWithStrictCost() *cel.CompositionEnv {
    56  	lazyCompositionEnvTemplateWithStrictCostInit.Do(func() {
    57  		env, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
    58  		if err != nil {
    59  			panic(err)
    60  		}
    61  		lazyCompositionEnvTemplateWithStrictCost = env
    62  	})
    63  	return lazyCompositionEnvTemplateWithStrictCost
    64  }
    65  
    66  func getCompositionEnvTemplateWithoutStrictCost() *cel.CompositionEnv {
    67  	lazyCompositionEnvTemplateWithoutStrictCostInit.Do(func() {
    68  		env, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), false))
    69  		if err != nil {
    70  			panic(err)
    71  		}
    72  		lazyCompositionEnvTemplateWithoutStrictCost = env
    73  	})
    74  	return lazyCompositionEnvTemplateWithoutStrictCost
    75  }
    76  
    77  // Register registers a plugin
    78  func Register(plugins *admission.Plugins) {
    79  	plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
    80  		return NewPlugin(configFile), nil
    81  	})
    82  }
    83  
    84  // Plugin is an implementation of admission.Interface.
    85  type Policy = v1.ValidatingAdmissionPolicy
    86  type PolicyBinding = v1.ValidatingAdmissionPolicyBinding
    87  type PolicyEvaluator = Validator
    88  type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
    89  
    90  type Plugin struct {
    91  	*generic.Plugin[PolicyHook]
    92  }
    93  
    94  var _ admission.Interface = &Plugin{}
    95  var _ admission.ValidationInterface = &Plugin{}
    96  var _ initializer.WantsFeatures = &Plugin{}
    97  var _ initializer.WantsExcludedAdmissionResources = &Plugin{}
    98  
    99  func NewPlugin(_ io.Reader) *Plugin {
   100  	handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
   101  
   102  	return &Plugin{
   103  		Plugin: generic.NewPlugin(
   104  			handler,
   105  			func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
   106  				return generic.NewPolicySource(
   107  					f.Admissionregistration().V1().ValidatingAdmissionPolicies().Informer(),
   108  					f.Admissionregistration().V1().ValidatingAdmissionPolicyBindings().Informer(),
   109  					NewValidatingAdmissionPolicyAccessor,
   110  					NewValidatingAdmissionPolicyBindingAccessor,
   111  					compilePolicy,
   112  					f,
   113  					dynamicClient,
   114  					restMapper,
   115  				)
   116  			},
   117  			func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
   118  				return NewDispatcher(a, generic.NewPolicyMatcher(m))
   119  			},
   120  		),
   121  	}
   122  }
   123  
   124  // Validate makes an admission decision based on the request attributes.
   125  func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
   126  	return a.Plugin.Dispatch(ctx, attr, o)
   127  }
   128  
   129  func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
   130  	a.Plugin.SetEnabled(featureGates.Enabled(features.ValidatingAdmissionPolicy))
   131  }
   132  
   133  func compilePolicy(policy *Policy) Validator {
   134  	hasParam := false
   135  	if policy.Spec.ParamKind != nil {
   136  		hasParam = true
   137  	}
   138  	strictCost := utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP)
   139  	optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true, StrictCost: strictCost}
   140  	expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false, StrictCost: strictCost}
   141  	failurePolicy := policy.Spec.FailurePolicy
   142  	var matcher matchconditions.Matcher = nil
   143  	matchConditions := policy.Spec.MatchConditions
   144  	var compositionEnvTemplate *cel.CompositionEnv
   145  	if strictCost {
   146  		compositionEnvTemplate = getCompositionEnvTemplateWithStrictCost()
   147  	} else {
   148  		compositionEnvTemplate = getCompositionEnvTemplateWithoutStrictCost()
   149  	}
   150  	filterCompiler := cel.NewCompositedCompilerFromTemplate(compositionEnvTemplate)
   151  	filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions)
   152  
   153  	if len(matchConditions) > 0 {
   154  		matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
   155  		for i := range matchConditions {
   156  			matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
   157  		}
   158  		matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
   159  	}
   160  	res := NewValidator(
   161  		filterCompiler.Compile(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
   162  		matcher,
   163  		filterCompiler.Compile(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
   164  		filterCompiler.Compile(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
   165  		failurePolicy,
   166  	)
   167  
   168  	return res
   169  }
   170  
   171  func convertv1Validations(inputValidations []v1.Validation) []cel.ExpressionAccessor {
   172  	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
   173  	for i, validation := range inputValidations {
   174  		validation := ValidationCondition{
   175  			Expression: validation.Expression,
   176  			Message:    validation.Message,
   177  			Reason:     validation.Reason,
   178  		}
   179  		celExpressionAccessor[i] = &validation
   180  	}
   181  	return celExpressionAccessor
   182  }
   183  
   184  func convertv1MessageExpressions(inputValidations []v1.Validation) []cel.ExpressionAccessor {
   185  	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
   186  	for i, validation := range inputValidations {
   187  		if validation.MessageExpression != "" {
   188  			condition := MessageExpressionCondition{
   189  				MessageExpression: validation.MessageExpression,
   190  			}
   191  			celExpressionAccessor[i] = &condition
   192  		}
   193  	}
   194  	return celExpressionAccessor
   195  }
   196  
   197  func convertv1AuditAnnotations(inputValidations []v1.AuditAnnotation) []cel.ExpressionAccessor {
   198  	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
   199  	for i, validation := range inputValidations {
   200  		validation := AuditAnnotationCondition{
   201  			Key:             validation.Key,
   202  			ValueExpression: validation.ValueExpression,
   203  		}
   204  		celExpressionAccessor[i] = &validation
   205  	}
   206  	return celExpressionAccessor
   207  }
   208  
   209  func convertv1beta1Variables(variables []v1.Variable) []cel.NamedExpressionAccessor {
   210  	namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
   211  	for i, variable := range variables {
   212  		namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
   213  	}
   214  	return namedExpressions
   215  }