refactor: implement VAP off of policy plugin fw
This commit is contained in:
		@@ -0,0 +1,95 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2024 The Kubernetes Authors.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package validating
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"k8s.io/api/admissionregistration/v1beta1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/types"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewValidatingAdmissionPolicyAccessor(obj *v1beta1.ValidatingAdmissionPolicy) generic.PolicyAccessor {
 | 
			
		||||
	return &validatingAdmissionPolicyAccessor{
 | 
			
		||||
		ValidatingAdmissionPolicy: obj,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewValidatingAdmissionPolicyBindingAccessor(obj *v1beta1.ValidatingAdmissionPolicyBinding) generic.BindingAccessor {
 | 
			
		||||
	return &validatingAdmissionPolicyBindingAccessor{
 | 
			
		||||
		ValidatingAdmissionPolicyBinding: obj,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type validatingAdmissionPolicyAccessor struct {
 | 
			
		||||
	*v1beta1.ValidatingAdmissionPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *validatingAdmissionPolicyAccessor) GetNamespace() string {
 | 
			
		||||
	return v.Namespace
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *validatingAdmissionPolicyAccessor) GetName() string {
 | 
			
		||||
	return v.Name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *validatingAdmissionPolicyAccessor) GetParamKind() *schema.GroupVersionKind {
 | 
			
		||||
	paramKind := v.Spec.ParamKind
 | 
			
		||||
	if paramKind == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	groupVersion, err := schema.ParseGroupVersion(paramKind.APIVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// A validatingadmissionpolicy which passes validation should have
 | 
			
		||||
		// a parseable APIVersion for its ParamKind, so this should never happen
 | 
			
		||||
		// if the policy is valid.
 | 
			
		||||
		//
 | 
			
		||||
		// Return a bogus but non-nil GVK that will throw an error about the
 | 
			
		||||
		// invalid APIVersion when the param is looked up.
 | 
			
		||||
		return &schema.GroupVersionKind{
 | 
			
		||||
			Group:   paramKind.APIVersion,
 | 
			
		||||
			Version: "",
 | 
			
		||||
			Kind:    paramKind.Kind,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &schema.GroupVersionKind{
 | 
			
		||||
		Group:   groupVersion.Group,
 | 
			
		||||
		Version: groupVersion.Version,
 | 
			
		||||
		Kind:    paramKind.Kind,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type validatingAdmissionPolicyBindingAccessor struct {
 | 
			
		||||
	*v1beta1.ValidatingAdmissionPolicyBinding
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *validatingAdmissionPolicyBindingAccessor) GetNamespace() string {
 | 
			
		||||
	return v.Namespace
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *validatingAdmissionPolicyBindingAccessor) GetName() string {
 | 
			
		||||
	return v.Name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (v *validatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
 | 
			
		||||
	return types.NamespacedName{
 | 
			
		||||
		Namespace: "",
 | 
			
		||||
		Name:      v.Spec.PolicyName,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,197 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2022 The Kubernetes Authors.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package validating
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apimachinery/pkg/api/meta"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authorization/authorizer"
 | 
			
		||||
	"k8s.io/apiserver/pkg/features"
 | 
			
		||||
	"k8s.io/client-go/dynamic"
 | 
			
		||||
	"k8s.io/component-base/featuregate"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/initializer"
 | 
			
		||||
	"k8s.io/client-go/informers"
 | 
			
		||||
	"k8s.io/client-go/kubernetes"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
////////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
// Plugin Definition
 | 
			
		||||
////////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
 | 
			
		||||
// Definition for CEL admission plugin. This is the entry point into the
 | 
			
		||||
// CEL admission control system.
 | 
			
		||||
//
 | 
			
		||||
// Each plugin is asked to validate every object update.
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// PluginName indicates the name of admission plug-in
 | 
			
		||||
	PluginName = "ValidatingAdmissionPolicy"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Register registers a plugin
 | 
			
		||||
func Register(plugins *admission.Plugins) {
 | 
			
		||||
	plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
 | 
			
		||||
		return NewPlugin()
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
////////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
// Plugin Initialization & Dependency Injection
 | 
			
		||||
////////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
 | 
			
		||||
type celAdmissionPlugin struct {
 | 
			
		||||
	*admission.Handler
 | 
			
		||||
	evaluator CELPolicyEvaluator
 | 
			
		||||
 | 
			
		||||
	inspectedFeatureGates bool
 | 
			
		||||
	enabled               bool
 | 
			
		||||
 | 
			
		||||
	// Injected Dependencies
 | 
			
		||||
	informerFactory informers.SharedInformerFactory
 | 
			
		||||
	client          kubernetes.Interface
 | 
			
		||||
	restMapper      meta.RESTMapper
 | 
			
		||||
	dynamicClient   dynamic.Interface
 | 
			
		||||
	stopCh          <-chan struct{}
 | 
			
		||||
	authorizer      authorizer.Authorizer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
 | 
			
		||||
var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
 | 
			
		||||
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
 | 
			
		||||
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
 | 
			
		||||
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
 | 
			
		||||
var _ initializer.WantsAuthorizer = &celAdmissionPlugin{}
 | 
			
		||||
var _ admission.InitializationValidator = &celAdmissionPlugin{}
 | 
			
		||||
var _ admission.ValidationInterface = &celAdmissionPlugin{}
 | 
			
		||||
 | 
			
		||||
func NewPlugin() (admission.Interface, error) {
 | 
			
		||||
	return &celAdmissionPlugin{
 | 
			
		||||
		Handler: admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
 | 
			
		||||
	c.informerFactory = f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionPlugin) SetExternalKubeClientSet(client kubernetes.Interface) {
 | 
			
		||||
	c.client = client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionPlugin) SetRESTMapper(mapper meta.RESTMapper) {
 | 
			
		||||
	c.restMapper = mapper
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionPlugin) SetDynamicClient(client dynamic.Interface) {
 | 
			
		||||
	c.dynamicClient = client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
 | 
			
		||||
	c.stopCh = stopCh
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) {
 | 
			
		||||
	c.authorizer = authorizer
 | 
			
		||||
}
 | 
			
		||||
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
 | 
			
		||||
	if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
 | 
			
		||||
		c.enabled = true
 | 
			
		||||
	}
 | 
			
		||||
	c.inspectedFeatureGates = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
 | 
			
		||||
func (c *celAdmissionPlugin) ValidateInitialization() error {
 | 
			
		||||
	if !c.inspectedFeatureGates {
 | 
			
		||||
		return fmt.Errorf("%s did not see feature gates", PluginName)
 | 
			
		||||
	}
 | 
			
		||||
	if !c.enabled {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if c.informerFactory == nil {
 | 
			
		||||
		return errors.New("missing informer factory")
 | 
			
		||||
	}
 | 
			
		||||
	if c.client == nil {
 | 
			
		||||
		return errors.New("missing kubernetes client")
 | 
			
		||||
	}
 | 
			
		||||
	if c.restMapper == nil {
 | 
			
		||||
		return errors.New("missing rest mapper")
 | 
			
		||||
	}
 | 
			
		||||
	if c.dynamicClient == nil {
 | 
			
		||||
		return errors.New("missing dynamic client")
 | 
			
		||||
	}
 | 
			
		||||
	if c.stopCh == nil {
 | 
			
		||||
		return errors.New("missing stop channel")
 | 
			
		||||
	}
 | 
			
		||||
	if c.authorizer == nil {
 | 
			
		||||
		return errors.New("missing authorizer")
 | 
			
		||||
	}
 | 
			
		||||
	c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient, c.authorizer)
 | 
			
		||||
	if err := c.evaluator.ValidateInitialization(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.SetReadyFunc(c.evaluator.HasSynced)
 | 
			
		||||
	go c.evaluator.Run(c.stopCh)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
////////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
// admission.ValidationInterface
 | 
			
		||||
////////////////////////////////////////////////////////////////////////////////
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionPlugin) Handles(operation admission.Operation) bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionPlugin) Validate(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	a admission.Attributes,
 | 
			
		||||
	o admission.ObjectInterfaces,
 | 
			
		||||
) (err error) {
 | 
			
		||||
	if !c.enabled {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// isPolicyResource determines if an admission.Attributes object is describing
 | 
			
		||||
	// the admission of a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding
 | 
			
		||||
	if isPolicyResource(a) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !c.WaitForReady() {
 | 
			
		||||
		return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.evaluator.Validate(ctx, a, o)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isPolicyResource(attr admission.Attributes) bool {
 | 
			
		||||
	gvk := attr.GetResource()
 | 
			
		||||
	if gvk.Group == "admissionregistration.k8s.io" {
 | 
			
		||||
		if gvk.Resource == "validatingadmissionpolicies" || gvk.Resource == "validatingadmissionpolicybindings" {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,551 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2022 The Kubernetes Authors.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package validating
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	v1 "k8s.io/api/admissionregistration/v1"
 | 
			
		||||
	"k8s.io/api/admissionregistration/v1beta1"
 | 
			
		||||
	corev1 "k8s.io/api/core/v1"
 | 
			
		||||
	apiequality "k8s.io/apimachinery/pkg/api/equality"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/api/meta"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
			
		||||
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/cel"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic"
 | 
			
		||||
	celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
 | 
			
		||||
	"k8s.io/apiserver/pkg/cel/environment"
 | 
			
		||||
	"k8s.io/client-go/dynamic"
 | 
			
		||||
	"k8s.io/client-go/dynamic/dynamicinformer"
 | 
			
		||||
	"k8s.io/client-go/informers"
 | 
			
		||||
	"k8s.io/client-go/kubernetes"
 | 
			
		||||
	"k8s.io/client-go/tools/cache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type policyController struct {
 | 
			
		||||
	once                        sync.Once
 | 
			
		||||
	context                     context.Context
 | 
			
		||||
	dynamicClient               dynamic.Interface
 | 
			
		||||
	informerFactory             informers.SharedInformerFactory
 | 
			
		||||
	restMapper                  meta.RESTMapper
 | 
			
		||||
	policyDefinitionsController generic.Controller[*v1beta1.ValidatingAdmissionPolicy]
 | 
			
		||||
	policyBindingController     generic.Controller[*v1beta1.ValidatingAdmissionPolicyBinding]
 | 
			
		||||
 | 
			
		||||
	// Provided to the policy's Compile function as an injected dependency to
 | 
			
		||||
	// assist with compiling its expressions to CEL
 | 
			
		||||
	// pass nil to create filter compiler in demand
 | 
			
		||||
	filterCompiler cel.FilterCompiler
 | 
			
		||||
 | 
			
		||||
	matcher Matcher
 | 
			
		||||
 | 
			
		||||
	newValidator
 | 
			
		||||
 | 
			
		||||
	client kubernetes.Interface
 | 
			
		||||
	// Lock which protects
 | 
			
		||||
	// All Below fields
 | 
			
		||||
	// All above fields should be assumed constant
 | 
			
		||||
	mutex sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	cachedPolicies []policyData
 | 
			
		||||
 | 
			
		||||
	// controller and metadata
 | 
			
		||||
	paramsCRDControllers map[v1beta1.ParamKind]*paramInfo
 | 
			
		||||
 | 
			
		||||
	// Index for each definition namespace/name, contains all binding
 | 
			
		||||
	// namespace/names known to exist for that definition
 | 
			
		||||
	definitionInfo map[namespacedName]*definitionInfo
 | 
			
		||||
 | 
			
		||||
	// Index for each bindings namespace/name. Contains compiled templates
 | 
			
		||||
	// for the binding depending on the policy/param combination.
 | 
			
		||||
	bindingInfos map[namespacedName]*bindingInfo
 | 
			
		||||
 | 
			
		||||
	// Map from namespace/name of a definition to a set of namespace/name
 | 
			
		||||
	// of bindings which depend on it.
 | 
			
		||||
	// All keys must have at least one dependent binding
 | 
			
		||||
	// All binding names MUST exist as a key bindingInfos
 | 
			
		||||
	definitionsToBindings map[namespacedName]sets.Set[namespacedName]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type newValidator func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failurePolicy *v1.FailurePolicyType) Validator
 | 
			
		||||
 | 
			
		||||
func newPolicyController(
 | 
			
		||||
	restMapper meta.RESTMapper,
 | 
			
		||||
	client kubernetes.Interface,
 | 
			
		||||
	dynamicClient dynamic.Interface,
 | 
			
		||||
	informerFactory informers.SharedInformerFactory,
 | 
			
		||||
	filterCompiler cel.FilterCompiler,
 | 
			
		||||
	matcher Matcher,
 | 
			
		||||
	policiesInformer generic.Informer[*v1beta1.ValidatingAdmissionPolicy],
 | 
			
		||||
	bindingsInformer generic.Informer[*v1beta1.ValidatingAdmissionPolicyBinding],
 | 
			
		||||
) *policyController {
 | 
			
		||||
	res := &policyController{}
 | 
			
		||||
	*res = policyController{
 | 
			
		||||
		filterCompiler:        filterCompiler,
 | 
			
		||||
		definitionInfo:        make(map[namespacedName]*definitionInfo),
 | 
			
		||||
		bindingInfos:          make(map[namespacedName]*bindingInfo),
 | 
			
		||||
		paramsCRDControllers:  make(map[v1beta1.ParamKind]*paramInfo),
 | 
			
		||||
		definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
 | 
			
		||||
		matcher:               matcher,
 | 
			
		||||
		newValidator:          NewValidator,
 | 
			
		||||
		policyDefinitionsController: generic.NewController(
 | 
			
		||||
			policiesInformer,
 | 
			
		||||
			res.reconcilePolicyDefinition,
 | 
			
		||||
			generic.ControllerOptions{
 | 
			
		||||
				Workers: 1,
 | 
			
		||||
				Name:    "cel-policy-definitions",
 | 
			
		||||
			},
 | 
			
		||||
		),
 | 
			
		||||
		policyBindingController: generic.NewController(
 | 
			
		||||
			bindingsInformer,
 | 
			
		||||
			res.reconcilePolicyBinding,
 | 
			
		||||
			generic.ControllerOptions{
 | 
			
		||||
				Workers: 1,
 | 
			
		||||
				Name:    "cel-policy-bindings",
 | 
			
		||||
			},
 | 
			
		||||
		),
 | 
			
		||||
		restMapper:      restMapper,
 | 
			
		||||
		dynamicClient:   dynamicClient,
 | 
			
		||||
		informerFactory: informerFactory,
 | 
			
		||||
		client:          client,
 | 
			
		||||
	}
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *policyController) Run(ctx context.Context) {
 | 
			
		||||
	// Only support being run once
 | 
			
		||||
	c.once.Do(func() {
 | 
			
		||||
		c.context = ctx
 | 
			
		||||
 | 
			
		||||
		wg := sync.WaitGroup{}
 | 
			
		||||
 | 
			
		||||
		wg.Add(1)
 | 
			
		||||
		go func() {
 | 
			
		||||
			defer wg.Done()
 | 
			
		||||
			c.policyDefinitionsController.Run(ctx)
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		wg.Add(1)
 | 
			
		||||
		go func() {
 | 
			
		||||
			defer wg.Done()
 | 
			
		||||
			c.policyBindingController.Run(ctx)
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		<-ctx.Done()
 | 
			
		||||
		wg.Wait()
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *policyController) HasSynced() bool {
 | 
			
		||||
	return c.policyDefinitionsController.HasSynced() && c.policyBindingController.HasSynced()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *policyController) reconcilePolicyDefinition(namespace, name string, definition *v1beta1.ValidatingAdmissionPolicy) error {
 | 
			
		||||
	c.mutex.Lock()
 | 
			
		||||
	defer c.mutex.Unlock()
 | 
			
		||||
	err := c.reconcilePolicyDefinitionSpec(namespace, name, definition)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *policyController) reconcilePolicyDefinitionSpec(namespace, name string, definition *v1beta1.ValidatingAdmissionPolicy) error {
 | 
			
		||||
	c.cachedPolicies = nil // invalidate cachedPolicies
 | 
			
		||||
 | 
			
		||||
	// Namespace for policydefinition is empty.
 | 
			
		||||
	nn := getNamespaceName(namespace, name)
 | 
			
		||||
	info, ok := c.definitionInfo[nn]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		info = &definitionInfo{}
 | 
			
		||||
		c.definitionInfo[nn] = info
 | 
			
		||||
		// TODO(DangerOnTheRanger): add support for "warn" being a valid enforcementAction
 | 
			
		||||
		celmetrics.Metrics.ObserveDefinition(context.TODO(), "active", "deny")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Skip reconcile if the spec of the definition is unchanged and had a
 | 
			
		||||
	// successful previous sync
 | 
			
		||||
	if info.configurationError == nil && info.lastReconciledValue != nil && definition != nil &&
 | 
			
		||||
		apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, definition.Spec) {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var paramSource *v1beta1.ParamKind
 | 
			
		||||
	if definition != nil {
 | 
			
		||||
		paramSource = definition.Spec.ParamKind
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If param source has changed, remove definition as dependent of old params
 | 
			
		||||
	// If there are no more dependents of old param, stop and clean up controller
 | 
			
		||||
	if info.lastReconciledValue != nil && info.lastReconciledValue.Spec.ParamKind != nil {
 | 
			
		||||
		oldParamSource := *info.lastReconciledValue.Spec.ParamKind
 | 
			
		||||
 | 
			
		||||
		// If we are:
 | 
			
		||||
		//	- switching from having a param to not having a param (includes deletion)
 | 
			
		||||
		//	- or from having a param to a different one
 | 
			
		||||
		// we remove dependency on the controller.
 | 
			
		||||
		if paramSource == nil || *paramSource != oldParamSource {
 | 
			
		||||
			if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok {
 | 
			
		||||
				oldParamInfo.dependentDefinitions.Delete(nn)
 | 
			
		||||
				if len(oldParamInfo.dependentDefinitions) == 0 {
 | 
			
		||||
					oldParamInfo.stop()
 | 
			
		||||
					delete(c.paramsCRDControllers, oldParamSource)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Reset all previously compiled evaluators in case something relevant in
 | 
			
		||||
	// definition has changed.
 | 
			
		||||
	for key := range c.definitionsToBindings[nn] {
 | 
			
		||||
		bindingInfo := c.bindingInfos[key]
 | 
			
		||||
		bindingInfo.validator = nil
 | 
			
		||||
		c.bindingInfos[key] = bindingInfo
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if definition == nil {
 | 
			
		||||
		delete(c.definitionInfo, nn)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update definition info
 | 
			
		||||
	info.lastReconciledValue = definition
 | 
			
		||||
	info.configurationError = nil
 | 
			
		||||
 | 
			
		||||
	if paramSource == nil {
 | 
			
		||||
		// Skip setting up controller for empty param type
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	// find GVR for params
 | 
			
		||||
	// Parse param source into a GVK
 | 
			
		||||
 | 
			
		||||
	paramSourceGV, err := schema.ParseGroupVersion(paramSource.APIVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// Failed to resolve. Return error so we retry again (rate limited)
 | 
			
		||||
		// Save a record of this definition with an evaluator that unconditionally
 | 
			
		||||
		info.configurationError = fmt.Errorf("failed to parse apiVersion of paramKind '%v' with error: %w", paramSource.String(), err)
 | 
			
		||||
 | 
			
		||||
		// Return nil, since this error cannot be resolved by waiting more time
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	paramsGVR, err := c.restMapper.RESTMapping(schema.GroupKind{
 | 
			
		||||
		Group: paramSourceGV.Group,
 | 
			
		||||
		Kind:  paramSource.Kind,
 | 
			
		||||
	}, paramSourceGV.Version)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// Failed to resolve. Return error so we retry again (rate limited)
 | 
			
		||||
		// Save a record of this definition with an evaluator that unconditionally
 | 
			
		||||
		//
 | 
			
		||||
		info.configurationError = fmt.Errorf("failed to find resource referenced by paramKind: '%v'", paramSourceGV.WithKind(paramSource.Kind))
 | 
			
		||||
		return info.configurationError
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	paramInfo := c.ensureParamInfo(paramSource, paramsGVR)
 | 
			
		||||
	paramInfo.dependentDefinitions.Insert(nn)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Ensures that there is an informer started for the given GVK to be used as a
 | 
			
		||||
// param
 | 
			
		||||
func (c *policyController) ensureParamInfo(paramSource *v1beta1.ParamKind, mapping *meta.RESTMapping) *paramInfo {
 | 
			
		||||
	if info, ok := c.paramsCRDControllers[*paramSource]; ok {
 | 
			
		||||
		return info
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We are not watching this param. Start an informer for it.
 | 
			
		||||
	instanceContext, instanceCancel := context.WithCancel(c.context)
 | 
			
		||||
 | 
			
		||||
	var informer cache.SharedIndexInformer
 | 
			
		||||
 | 
			
		||||
	// Try to see if our provided informer factory has an informer for this type.
 | 
			
		||||
	// We assume the informer is already started, and starts all types associated
 | 
			
		||||
	// with it.
 | 
			
		||||
	if genericInformer, err := c.informerFactory.ForResource(mapping.Resource); err == nil {
 | 
			
		||||
		informer = genericInformer.Informer()
 | 
			
		||||
 | 
			
		||||
		// Ensure the informer is started
 | 
			
		||||
		// Use policyController's context rather than the instance context.
 | 
			
		||||
		// PolicyController context is expected to last until app shutdown
 | 
			
		||||
		// This is due to behavior of informerFactory which would cause the
 | 
			
		||||
		// informer to stop running once the context is cancelled, and
 | 
			
		||||
		// never started again.
 | 
			
		||||
		c.informerFactory.Start(c.context.Done())
 | 
			
		||||
	} else {
 | 
			
		||||
		// Dynamic JSON informer fallback.
 | 
			
		||||
		// Cannot use shared dynamic informer since it would be impossible
 | 
			
		||||
		// to clean CRD informers properly with multiple dependents
 | 
			
		||||
		// (cannot start ahead of time, and cannot track dependencies via stopCh)
 | 
			
		||||
		informer = dynamicinformer.NewFilteredDynamicInformer(
 | 
			
		||||
			c.dynamicClient,
 | 
			
		||||
			mapping.Resource,
 | 
			
		||||
			corev1.NamespaceAll,
 | 
			
		||||
			// Use same interval as is used for k8s typed sharedInformerFactory
 | 
			
		||||
			// https://github.com/kubernetes/kubernetes/blob/7e0923899fed622efbc8679cca6b000d43633e38/cmd/kube-apiserver/app/server.go#L430
 | 
			
		||||
			10*time.Minute,
 | 
			
		||||
			cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
 | 
			
		||||
			nil,
 | 
			
		||||
		).Informer()
 | 
			
		||||
		go informer.Run(instanceContext.Done())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller := generic.NewController(
 | 
			
		||||
		generic.NewInformer[runtime.Object](informer),
 | 
			
		||||
		c.reconcileParams,
 | 
			
		||||
		generic.ControllerOptions{
 | 
			
		||||
			Workers: 1,
 | 
			
		||||
			Name:    paramSource.String() + "-controller",
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	ret := ¶mInfo{
 | 
			
		||||
		controller:           controller,
 | 
			
		||||
		stop:                 instanceCancel,
 | 
			
		||||
		scope:                mapping.Scope,
 | 
			
		||||
		dependentDefinitions: sets.New[namespacedName](),
 | 
			
		||||
	}
 | 
			
		||||
	c.paramsCRDControllers[*paramSource] = ret
 | 
			
		||||
 | 
			
		||||
	go controller.Run(instanceContext)
 | 
			
		||||
	return ret
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *policyController) reconcilePolicyBinding(namespace, name string, binding *v1beta1.ValidatingAdmissionPolicyBinding) error {
 | 
			
		||||
	c.mutex.Lock()
 | 
			
		||||
	defer c.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	c.cachedPolicies = nil // invalidate cachedPolicies
 | 
			
		||||
 | 
			
		||||
	// Namespace for PolicyBinding is empty. In the future a namespaced binding
 | 
			
		||||
	// may be added
 | 
			
		||||
	// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
 | 
			
		||||
	nn := getNamespaceName(namespace, name)
 | 
			
		||||
	info, ok := c.bindingInfos[nn]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		info = &bindingInfo{}
 | 
			
		||||
		c.bindingInfos[nn] = info
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Skip if the spec of the binding is unchanged.
 | 
			
		||||
	if info.lastReconciledValue != nil && binding != nil &&
 | 
			
		||||
		apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, binding.Spec) {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var oldNamespacedDefinitionName namespacedName
 | 
			
		||||
	if info.lastReconciledValue != nil {
 | 
			
		||||
		// All validating policies are cluster-scoped so have empty namespace
 | 
			
		||||
		oldNamespacedDefinitionName = getNamespaceName("", info.lastReconciledValue.Spec.PolicyName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var namespacedDefinitionName namespacedName
 | 
			
		||||
	if binding != nil {
 | 
			
		||||
		// All validating policies are cluster-scoped so have empty namespace
 | 
			
		||||
		namespacedDefinitionName = getNamespaceName("", binding.Spec.PolicyName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Remove record of binding from old definition if the referred policy
 | 
			
		||||
	// has changed
 | 
			
		||||
	if oldNamespacedDefinitionName != namespacedDefinitionName {
 | 
			
		||||
		if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok {
 | 
			
		||||
			dependentBindings.Delete(nn)
 | 
			
		||||
 | 
			
		||||
			// if there are no more dependent bindings, remove knowledge of the
 | 
			
		||||
			// definition altogether
 | 
			
		||||
			if len(dependentBindings) == 0 {
 | 
			
		||||
				delete(c.definitionsToBindings, oldNamespacedDefinitionName)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if binding == nil {
 | 
			
		||||
		delete(c.bindingInfos, nn)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add record of binding to new definition
 | 
			
		||||
	if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok {
 | 
			
		||||
		dependentBindings.Insert(nn)
 | 
			
		||||
	} else {
 | 
			
		||||
		c.definitionsToBindings[namespacedDefinitionName] = sets.New(nn)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Remove compiled template for old binding
 | 
			
		||||
	info.validator = nil
 | 
			
		||||
	info.lastReconciledValue = binding
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *policyController) reconcileParams(namespace, name string, params runtime.Object) error {
 | 
			
		||||
	// Do nothing.
 | 
			
		||||
	// When we add informational type checking we will need to compile in the
 | 
			
		||||
	// reconcile loops instead of lazily so we can add compiler errors / type
 | 
			
		||||
	// checker errors to the status of the resources.
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Fetches the latest set of policy data or recalculates it if it has changed
 | 
			
		||||
// since it was last fetched
 | 
			
		||||
func (c *policyController) latestPolicyData() []policyData {
 | 
			
		||||
	existing := func() []policyData {
 | 
			
		||||
		c.mutex.RLock()
 | 
			
		||||
		defer c.mutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
		return c.cachedPolicies
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if existing != nil {
 | 
			
		||||
		return existing
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.mutex.Lock()
 | 
			
		||||
	defer c.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	var res []policyData
 | 
			
		||||
	for definitionNN, definitionInfo := range c.definitionInfo {
 | 
			
		||||
		var bindingInfos []bindingInfo
 | 
			
		||||
		for bindingNN := range c.definitionsToBindings[definitionNN] {
 | 
			
		||||
			bindingInfo := c.bindingInfos[bindingNN]
 | 
			
		||||
			if bindingInfo.validator == nil && definitionInfo.configurationError == nil {
 | 
			
		||||
				hasParam := false
 | 
			
		||||
				if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
 | 
			
		||||
					hasParam = true
 | 
			
		||||
				}
 | 
			
		||||
				optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
 | 
			
		||||
				expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
 | 
			
		||||
				failurePolicy := convertv1beta1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy)
 | 
			
		||||
				var matcher matchconditions.Matcher = nil
 | 
			
		||||
				matchConditions := definitionInfo.lastReconciledValue.Spec.MatchConditions
 | 
			
		||||
 | 
			
		||||
				filterCompiler := c.filterCompiler
 | 
			
		||||
				if filterCompiler == nil {
 | 
			
		||||
					compositedCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
 | 
			
		||||
					if err == nil {
 | 
			
		||||
						filterCompiler = compositedCompiler
 | 
			
		||||
						compositedCompiler.CompileAndStoreVariables(convertv1beta1Variables(definitionInfo.lastReconciledValue.Spec.Variables), optionalVars, environment.StoredExpressions)
 | 
			
		||||
					} else {
 | 
			
		||||
						utilruntime.HandleError(err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				if len(matchConditions) > 0 {
 | 
			
		||||
					matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
 | 
			
		||||
					for i := range matchConditions {
 | 
			
		||||
						matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
 | 
			
		||||
					}
 | 
			
		||||
					matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", definitionInfo.lastReconciledValue.Name)
 | 
			
		||||
				}
 | 
			
		||||
				bindingInfo.validator = c.newValidator(
 | 
			
		||||
					filterCompiler.Compile(convertv1beta1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, environment.StoredExpressions),
 | 
			
		||||
					matcher,
 | 
			
		||||
					filterCompiler.Compile(convertv1beta1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
 | 
			
		||||
					filterCompiler.Compile(convertv1beta1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
 | 
			
		||||
					failurePolicy,
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
			bindingInfos = append(bindingInfos, *bindingInfo)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var pInfo paramInfo
 | 
			
		||||
		if paramKind := definitionInfo.lastReconciledValue.Spec.ParamKind; paramKind != nil {
 | 
			
		||||
			if info, ok := c.paramsCRDControllers[*paramKind]; ok {
 | 
			
		||||
				pInfo = *info
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res = append(res, policyData{
 | 
			
		||||
			definitionInfo: *definitionInfo,
 | 
			
		||||
			paramInfo:      pInfo,
 | 
			
		||||
			bindings:       bindingInfos,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.cachedPolicies = res
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policyType *v1beta1.FailurePolicyType) *v1.FailurePolicyType {
 | 
			
		||||
	if policyType == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var v1FailPolicy v1.FailurePolicyType
 | 
			
		||||
	if *policyType == v1beta1.Fail {
 | 
			
		||||
		v1FailPolicy = v1.Fail
 | 
			
		||||
	} else if *policyType == v1beta1.Ignore {
 | 
			
		||||
		v1FailPolicy = v1.Ignore
 | 
			
		||||
	}
 | 
			
		||||
	return &v1FailPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1Validations(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
 | 
			
		||||
	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
 | 
			
		||||
	for i, validation := range inputValidations {
 | 
			
		||||
		validation := ValidationCondition{
 | 
			
		||||
			Expression: validation.Expression,
 | 
			
		||||
			Message:    validation.Message,
 | 
			
		||||
			Reason:     validation.Reason,
 | 
			
		||||
		}
 | 
			
		||||
		celExpressionAccessor[i] = &validation
 | 
			
		||||
	}
 | 
			
		||||
	return celExpressionAccessor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1MessageExpressions(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
 | 
			
		||||
	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
 | 
			
		||||
	for i, validation := range inputValidations {
 | 
			
		||||
		if validation.MessageExpression != "" {
 | 
			
		||||
			condition := MessageExpressionCondition{
 | 
			
		||||
				MessageExpression: validation.MessageExpression,
 | 
			
		||||
			}
 | 
			
		||||
			celExpressionAccessor[i] = &condition
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return celExpressionAccessor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1AuditAnnotations(inputValidations []v1beta1.AuditAnnotation) []cel.ExpressionAccessor {
 | 
			
		||||
	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
 | 
			
		||||
	for i, validation := range inputValidations {
 | 
			
		||||
		validation := AuditAnnotationCondition{
 | 
			
		||||
			Key:             validation.Key,
 | 
			
		||||
			ValueExpression: validation.ValueExpression,
 | 
			
		||||
		}
 | 
			
		||||
		celExpressionAccessor[i] = &validation
 | 
			
		||||
	}
 | 
			
		||||
	return celExpressionAccessor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1Variables(variables []v1beta1.Variable) []cel.NamedExpressionAccessor {
 | 
			
		||||
	namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
 | 
			
		||||
	for i, variable := range variables {
 | 
			
		||||
		namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
 | 
			
		||||
	}
 | 
			
		||||
	return namedExpressions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getNamespaceName(namespace, name string) namespacedName {
 | 
			
		||||
	return namespacedName{
 | 
			
		||||
		namespace: namespace,
 | 
			
		||||
		name:      name,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -21,8 +21,6 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/api/admissionregistration/v1beta1"
 | 
			
		||||
@@ -34,47 +32,32 @@ import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
			
		||||
	utiljson "k8s.io/apimachinery/pkg/util/json"
 | 
			
		||||
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/wait"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
 | 
			
		||||
	celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
 | 
			
		||||
	celconfig "k8s.io/apiserver/pkg/apis/cel"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authorization/authorizer"
 | 
			
		||||
	"k8s.io/apiserver/pkg/warning"
 | 
			
		||||
	"k8s.io/client-go/dynamic"
 | 
			
		||||
	"k8s.io/client-go/informers"
 | 
			
		||||
	"k8s.io/client-go/kubernetes"
 | 
			
		||||
	"k8s.io/client-go/tools/cache"
 | 
			
		||||
	"k8s.io/klog/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var _ CELPolicyEvaluator = &celAdmissionController{}
 | 
			
		||||
 | 
			
		||||
// celAdmissionController is the top-level controller for admission control using CEL
 | 
			
		||||
// it is responsible for watching policy definitions, bindings, and config param CRDs
 | 
			
		||||
type celAdmissionController struct {
 | 
			
		||||
	// Controller which manages book-keeping for the cluster's dynamic policy
 | 
			
		||||
	// information.
 | 
			
		||||
	policyController *policyController
 | 
			
		||||
 | 
			
		||||
	// atomic []policyData
 | 
			
		||||
	// list of every known policy definition, and all informatoin required to
 | 
			
		||||
	// validate its bindings against an object.
 | 
			
		||||
	// A snapshot of the current policy configuration is synced with this field
 | 
			
		||||
	// asynchronously
 | 
			
		||||
	definitions atomic.Value
 | 
			
		||||
 | 
			
		||||
	authz authorizer.Authorizer
 | 
			
		||||
type dispatcher struct {
 | 
			
		||||
	matcher Matcher
 | 
			
		||||
	authz   authorizer.Authorizer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Everything someone might need to validate a single ValidatingPolicyDefinition
 | 
			
		||||
// against all of its registered bindings.
 | 
			
		||||
type policyData struct {
 | 
			
		||||
	definitionInfo
 | 
			
		||||
	paramInfo
 | 
			
		||||
	bindings []bindingInfo
 | 
			
		||||
var _ generic.Dispatcher[PolicyHook] = &dispatcher{}
 | 
			
		||||
 | 
			
		||||
func NewDispatcher(
 | 
			
		||||
	authorizer authorizer.Authorizer,
 | 
			
		||||
	matcher Matcher,
 | 
			
		||||
) generic.Dispatcher[PolicyHook] {
 | 
			
		||||
	return &dispatcher{
 | 
			
		||||
		matcher: matcher,
 | 
			
		||||
		authz:   authorizer,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
 | 
			
		||||
@@ -85,110 +68,8 @@ type policyDecisionWithMetadata struct {
 | 
			
		||||
	Binding    *v1beta1.ValidatingAdmissionPolicyBinding
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// namespaceName is used as a key in definitionInfo and bindingInfos
 | 
			
		||||
type namespacedName struct {
 | 
			
		||||
	namespace, name string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type definitionInfo struct {
 | 
			
		||||
	// Error about the state of the definition's configuration and the cluster
 | 
			
		||||
	// preventing its enforcement or compilation.
 | 
			
		||||
	// Reset every reconciliation
 | 
			
		||||
	configurationError error
 | 
			
		||||
 | 
			
		||||
	// Last value seen by this controller to be used in policy enforcement
 | 
			
		||||
	// May not be nil
 | 
			
		||||
	lastReconciledValue *v1beta1.ValidatingAdmissionPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type bindingInfo struct {
 | 
			
		||||
	// Compiled CEL expression turned into an validator
 | 
			
		||||
	validator Validator
 | 
			
		||||
 | 
			
		||||
	// Last value seen by this controller to be used in policy enforcement
 | 
			
		||||
	// May not be nil
 | 
			
		||||
	lastReconciledValue *v1beta1.ValidatingAdmissionPolicyBinding
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type paramInfo struct {
 | 
			
		||||
	// Controller which is watching this param CRD
 | 
			
		||||
	controller generic.Controller[runtime.Object]
 | 
			
		||||
 | 
			
		||||
	// Function to call to stop the informer and clean up the controller
 | 
			
		||||
	stop func()
 | 
			
		||||
 | 
			
		||||
	// Whether this param is cluster or namespace scoped
 | 
			
		||||
	scope meta.RESTScope
 | 
			
		||||
 | 
			
		||||
	// Policy Definitions which refer to this param CRD
 | 
			
		||||
	dependentDefinitions sets.Set[namespacedName]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAdmissionController(
 | 
			
		||||
	// Injected Dependencies
 | 
			
		||||
	informerFactory informers.SharedInformerFactory,
 | 
			
		||||
	client kubernetes.Interface,
 | 
			
		||||
	restMapper meta.RESTMapper,
 | 
			
		||||
	dynamicClient dynamic.Interface,
 | 
			
		||||
	authz authorizer.Authorizer,
 | 
			
		||||
) CELPolicyEvaluator {
 | 
			
		||||
	return &celAdmissionController{
 | 
			
		||||
		definitions: atomic.Value{},
 | 
			
		||||
		policyController: newPolicyController(
 | 
			
		||||
			restMapper,
 | 
			
		||||
			client,
 | 
			
		||||
			dynamicClient,
 | 
			
		||||
			informerFactory,
 | 
			
		||||
			nil,
 | 
			
		||||
			NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)),
 | 
			
		||||
			generic.NewInformer[*v1beta1.ValidatingAdmissionPolicy](
 | 
			
		||||
				informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicies().Informer()),
 | 
			
		||||
			generic.NewInformer[*v1beta1.ValidatingAdmissionPolicyBinding](
 | 
			
		||||
				informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicyBindings().Informer()),
 | 
			
		||||
		),
 | 
			
		||||
		authz: authz,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	wg := sync.WaitGroup{}
 | 
			
		||||
 | 
			
		||||
	wg.Add(1)
 | 
			
		||||
	go func() {
 | 
			
		||||
		defer wg.Done()
 | 
			
		||||
		c.policyController.Run(ctx)
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	wg.Add(1)
 | 
			
		||||
	go func() {
 | 
			
		||||
		defer wg.Done()
 | 
			
		||||
 | 
			
		||||
		// Wait indefinitely until policies/bindings are listed & handled before
 | 
			
		||||
		// allowing policies to be refreshed
 | 
			
		||||
		if !cache.WaitForNamedCacheSync("cel-admission-controller", ctx.Done(), c.policyController.HasSynced) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Loop every 1 second until context is cancelled, refreshing policies
 | 
			
		||||
		wait.Until(c.refreshPolicies, 1*time.Second, ctx.Done())
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	<-stopCh
 | 
			
		||||
	cancel()
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const maxAuditAnnotationValueLength = 10 * 1024
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionController) Validate(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	a admission.Attributes,
 | 
			
		||||
	o admission.ObjectInterfaces,
 | 
			
		||||
) (err error) {
 | 
			
		||||
	if !c.HasSynced() {
 | 
			
		||||
		return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
 | 
			
		||||
	}
 | 
			
		||||
// Dispatch implements generic.Dispatcher.
 | 
			
		||||
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
 | 
			
		||||
 | 
			
		||||
	var deniedDecisions []policyDecisionWithMetadata
 | 
			
		||||
 | 
			
		||||
@@ -232,19 +113,18 @@ func (c *celAdmissionController) Validate(
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	policyDatas := c.definitions.Load().([]policyData)
 | 
			
		||||
 | 
			
		||||
	authz := newCachingAuthorizer(c.authz)
 | 
			
		||||
 | 
			
		||||
	for _, definitionInfo := range policyDatas {
 | 
			
		||||
	for _, hook := range hooks {
 | 
			
		||||
		// versionedAttributes will be set to non-nil inside of the loop, but
 | 
			
		||||
		// is scoped outside of the param loop so we only convert once. We defer
 | 
			
		||||
		// conversion so that it is only performed when we know a policy matches,
 | 
			
		||||
		// saving the cost of converting non-matching requests.
 | 
			
		||||
		var versionedAttr *admission.VersionedAttributes
 | 
			
		||||
 | 
			
		||||
		definition := definitionInfo.lastReconciledValue
 | 
			
		||||
		matches, matchResource, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition)
 | 
			
		||||
		definition := hook.Policy
 | 
			
		||||
		matches, matchResource, matchKind, err := c.matcher.DefinitionMatches(a, o, definition)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			// Configuration error.
 | 
			
		||||
			addConfigError(err, definition, nil)
 | 
			
		||||
@@ -253,18 +133,17 @@ func (c *celAdmissionController) Validate(
 | 
			
		||||
		if !matches {
 | 
			
		||||
			// Policy definition does not match request
 | 
			
		||||
			continue
 | 
			
		||||
		} else if definitionInfo.configurationError != nil {
 | 
			
		||||
		} else if hook.ConfigurationError != nil {
 | 
			
		||||
			// Configuration error.
 | 
			
		||||
			addConfigError(definitionInfo.configurationError, definition, nil)
 | 
			
		||||
			addConfigError(hook.ConfigurationError, definition, nil)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auditAnnotationCollector := newAuditAnnotationCollector()
 | 
			
		||||
		for _, bindingInfo := range definitionInfo.bindings {
 | 
			
		||||
		for _, binding := range hook.Bindings {
 | 
			
		||||
			// If the key is inside dependentBindings, there is guaranteed to
 | 
			
		||||
			// be a bindingInfo for it
 | 
			
		||||
			binding := bindingInfo.lastReconciledValue
 | 
			
		||||
			matches, err := c.policyController.matcher.BindingMatches(a, o, binding)
 | 
			
		||||
			matches, err := c.matcher.BindingMatches(a, o, binding)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// Configuration error.
 | 
			
		||||
				addConfigError(err, definition, binding)
 | 
			
		||||
@@ -274,7 +153,14 @@ func (c *celAdmissionController) Validate(
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			params, err := c.collectParams(definition.Spec.ParamKind, definitionInfo.paramInfo, binding.Spec.ParamRef, a.GetNamespace())
 | 
			
		||||
			params, err := c.collectParams(
 | 
			
		||||
				definition.Spec.ParamKind,
 | 
			
		||||
				hook.ParamInformer,
 | 
			
		||||
				hook.ParamScope,
 | 
			
		||||
				binding.Spec.ParamRef,
 | 
			
		||||
				a.GetNamespace(),
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				addConfigError(err, definition, binding)
 | 
			
		||||
				continue
 | 
			
		||||
@@ -303,7 +189,7 @@ func (c *celAdmissionController) Validate(
 | 
			
		||||
			// if it is cluster scoped, namespaceName will be empty
 | 
			
		||||
			// Otherwise, get the Namespace resource.
 | 
			
		||||
			if namespaceName != "" {
 | 
			
		||||
				namespace, err = c.policyController.matcher.GetNamespace(namespaceName)
 | 
			
		||||
				namespace, err = c.matcher.GetNamespace(namespaceName)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
@@ -323,7 +209,18 @@ func (c *celAdmissionController) Validate(
 | 
			
		||||
						nested: param,
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				validationResults = append(validationResults, bindingInfo.validator.Validate(ctx, matchResource, versionedAttr, p, namespace, celconfig.RuntimeCELCostBudget, authz))
 | 
			
		||||
 | 
			
		||||
				validationResults = append(validationResults,
 | 
			
		||||
					hook.Evaluator.Validate(
 | 
			
		||||
						ctx,
 | 
			
		||||
						matchResource,
 | 
			
		||||
						versionedAttr,
 | 
			
		||||
						p,
 | 
			
		||||
						namespace,
 | 
			
		||||
						celconfig.RuntimeCELCostBudget,
 | 
			
		||||
						authz,
 | 
			
		||||
					),
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, validationResult := range validationResults {
 | 
			
		||||
@@ -344,7 +241,7 @@ func (c *celAdmissionController) Validate(
 | 
			
		||||
								})
 | 
			
		||||
								celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
 | 
			
		||||
							case v1beta1.Audit:
 | 
			
		||||
								c.publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
 | 
			
		||||
								publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
 | 
			
		||||
								celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
 | 
			
		||||
							case v1beta1.Warn:
 | 
			
		||||
								warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
 | 
			
		||||
@@ -411,28 +308,30 @@ func (c *celAdmissionController) Validate(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns objects to use to evaluate the policy
 | 
			
		||||
func (c *celAdmissionController) collectParams(
 | 
			
		||||
// Copied with minor modification to account for slightly different arguments
 | 
			
		||||
func (c *dispatcher) collectParams(
 | 
			
		||||
	paramKind *v1beta1.ParamKind,
 | 
			
		||||
	info paramInfo,
 | 
			
		||||
	paramInformer informers.GenericInformer,
 | 
			
		||||
	paramScope meta.RESTScope,
 | 
			
		||||
	paramRef *v1beta1.ParamRef,
 | 
			
		||||
	namespace string,
 | 
			
		||||
) ([]runtime.Object, error) {
 | 
			
		||||
	// If definition has paramKind, paramRef is required in binding.
 | 
			
		||||
	// If definition has no paramKind, paramRef set in binding will be ignored.
 | 
			
		||||
	var params []runtime.Object
 | 
			
		||||
	var paramStore generic.NamespacedLister[runtime.Object]
 | 
			
		||||
	var paramStore cache.GenericNamespaceLister
 | 
			
		||||
 | 
			
		||||
	// Make sure the param kind is ready to use
 | 
			
		||||
	if paramKind != nil && paramRef != nil {
 | 
			
		||||
		if info.controller == nil {
 | 
			
		||||
		if paramInformer == nil {
 | 
			
		||||
			return nil, fmt.Errorf("paramKind kind `%v` not known",
 | 
			
		||||
				paramKind.String())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Set up cluster-scoped, or namespaced access to the params
 | 
			
		||||
		// "default" if not provided, and paramKind is namespaced
 | 
			
		||||
		paramStore = info.controller.Informer()
 | 
			
		||||
		if info.scope.Name() == meta.RESTScopeNameNamespace {
 | 
			
		||||
		paramStore = paramInformer.Lister()
 | 
			
		||||
		if paramScope.Name() == meta.RESTScopeNameNamespace {
 | 
			
		||||
			paramsNamespace := namespace
 | 
			
		||||
			if len(paramRef.Namespace) > 0 {
 | 
			
		||||
				paramsNamespace = paramRef.Namespace
 | 
			
		||||
@@ -442,16 +341,16 @@ func (c *celAdmissionController) collectParams(
 | 
			
		||||
				return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			paramStore = info.controller.Informer().Namespaced(paramsNamespace)
 | 
			
		||||
			paramStore = paramInformer.Lister().ByNamespace(paramsNamespace)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If the param informer for this admission policy has not yet
 | 
			
		||||
		// had time to perform an initial listing, don't attempt to use
 | 
			
		||||
		// it.
 | 
			
		||||
		timeoutCtx, cancel := context.WithTimeout(c.policyController.context, 1*time.Second)
 | 
			
		||||
		timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
 | 
			
		||||
		if !cache.WaitForCacheSync(timeoutCtx.Done(), info.controller.HasSynced) {
 | 
			
		||||
		if !cache.WaitForCacheSync(timeoutCtx.Done(), paramInformer.Informer().HasSynced) {
 | 
			
		||||
			return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
 | 
			
		||||
				paramKind.String())
 | 
			
		||||
		}
 | 
			
		||||
@@ -467,7 +366,7 @@ func (c *celAdmissionController) collectParams(
 | 
			
		||||
		// Policy ParamKind is set, but binding does not use it.
 | 
			
		||||
		// Validate with nil params
 | 
			
		||||
		return []runtime.Object{nil}, nil
 | 
			
		||||
	case len(paramRef.Namespace) > 0 && info.scope.Name() == meta.RESTScopeRoot.Name():
 | 
			
		||||
	case len(paramRef.Namespace) > 0 && paramScope.Name() == meta.RESTScopeRoot.Name():
 | 
			
		||||
		// Not allowed to set namespace for cluster-scoped param
 | 
			
		||||
		return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`")
 | 
			
		||||
 | 
			
		||||
@@ -527,10 +426,10 @@ func (c *celAdmissionController) collectParams(
 | 
			
		||||
	return params, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1beta1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
 | 
			
		||||
func publishValidationFailureAnnotation(binding *v1beta1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
 | 
			
		||||
	key := "validation.policy.admission.k8s.io/validation_failure"
 | 
			
		||||
	// Marshal to a list of failures since, in the future, we may need to support multiple failures
 | 
			
		||||
	valueJson, err := utiljson.Marshal([]validationFailureValue{{
 | 
			
		||||
	valueJSON, err := utiljson.Marshal([]ValidationFailureValue{{
 | 
			
		||||
		ExpressionIndex:   expressionIndex,
 | 
			
		||||
		Message:           decision.Message,
 | 
			
		||||
		ValidationActions: binding.Spec.ValidationActions,
 | 
			
		||||
@@ -540,27 +439,17 @@ func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1b
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err)
 | 
			
		||||
	}
 | 
			
		||||
	value := string(valueJson)
 | 
			
		||||
	value := string(valueJSON)
 | 
			
		||||
	if err := attributes.AddAnnotation(key, value); err != nil {
 | 
			
		||||
		klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionController) HasSynced() bool {
 | 
			
		||||
	return c.policyController.HasSynced() && c.definitions.Load() != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionController) ValidateInitialization() error {
 | 
			
		||||
	return c.policyController.matcher.ValidateInitialization()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *celAdmissionController) refreshPolicies() {
 | 
			
		||||
	c.definitions.Store(c.policyController.latestPolicyData())
 | 
			
		||||
}
 | 
			
		||||
const maxAuditAnnotationValueLength = 10 * 1024
 | 
			
		||||
 | 
			
		||||
// validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
 | 
			
		||||
// annotation value.
 | 
			
		||||
type validationFailureValue struct {
 | 
			
		||||
type ValidationFailureValue struct {
 | 
			
		||||
	Message           string                     `json:"message"`
 | 
			
		||||
	Policy            string                     `json:"policy"`
 | 
			
		||||
	Binding           string                     `json:"binding"`
 | 
			
		||||
@@ -0,0 +1,197 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2024 The Kubernetes Authors.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package validating
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	v1 "k8s.io/api/admissionregistration/v1"
 | 
			
		||||
	"k8s.io/api/admissionregistration/v1beta1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/api/meta"
 | 
			
		||||
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/initializer"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/cel"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
 | 
			
		||||
	"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authorization/authorizer"
 | 
			
		||||
	"k8s.io/apiserver/pkg/cel/environment"
 | 
			
		||||
	"k8s.io/apiserver/pkg/features"
 | 
			
		||||
	"k8s.io/client-go/dynamic"
 | 
			
		||||
	"k8s.io/client-go/informers"
 | 
			
		||||
	"k8s.io/client-go/kubernetes"
 | 
			
		||||
	"k8s.io/component-base/featuregate"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// PluginName indicates the name of admission plug-in
 | 
			
		||||
	PluginName = "ValidatingAdmissionPolicy"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Register registers a plugin
 | 
			
		||||
func Register(plugins *admission.Plugins) {
 | 
			
		||||
	plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
 | 
			
		||||
		return NewPlugin(configFile), nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Plugin is an implementation of admission.Interface.
 | 
			
		||||
type Policy = v1beta1.ValidatingAdmissionPolicy
 | 
			
		||||
type PolicyBinding = v1beta1.ValidatingAdmissionPolicyBinding
 | 
			
		||||
type PolicyEvaluator = Validator
 | 
			
		||||
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
 | 
			
		||||
 | 
			
		||||
type Plugin struct {
 | 
			
		||||
	*generic.Plugin[PolicyHook]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ admission.Interface = &Plugin{}
 | 
			
		||||
var _ admission.ValidationInterface = &Plugin{}
 | 
			
		||||
var _ initializer.WantsFeatures = &Plugin{}
 | 
			
		||||
 | 
			
		||||
func NewPlugin(_ io.Reader) *Plugin {
 | 
			
		||||
	handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
 | 
			
		||||
 | 
			
		||||
	return &Plugin{
 | 
			
		||||
		Plugin: generic.NewPlugin(
 | 
			
		||||
			handler,
 | 
			
		||||
			func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
 | 
			
		||||
				return generic.NewPolicySource(
 | 
			
		||||
					f.Admissionregistration().V1beta1().ValidatingAdmissionPolicies().Informer(),
 | 
			
		||||
					f.Admissionregistration().V1beta1().ValidatingAdmissionPolicyBindings().Informer(),
 | 
			
		||||
					NewValidatingAdmissionPolicyAccessor,
 | 
			
		||||
					NewValidatingAdmissionPolicyBindingAccessor,
 | 
			
		||||
					compilePolicy,
 | 
			
		||||
					f,
 | 
			
		||||
					dynamicClient,
 | 
			
		||||
					restMapper,
 | 
			
		||||
				)
 | 
			
		||||
			},
 | 
			
		||||
			func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
 | 
			
		||||
				return NewDispatcher(a, NewMatcher(m))
 | 
			
		||||
			},
 | 
			
		||||
		),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate makes an admission decision based on the request attributes.
 | 
			
		||||
func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
 | 
			
		||||
	return a.Plugin.Dispatch(ctx, attr, o)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
 | 
			
		||||
	a.Plugin.SetEnabled(featureGates.Enabled(features.ValidatingAdmissionPolicy))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func compilePolicy(policy *Policy) Validator {
 | 
			
		||||
	hasParam := false
 | 
			
		||||
	if policy.Spec.ParamKind != nil {
 | 
			
		||||
		hasParam = true
 | 
			
		||||
	}
 | 
			
		||||
	optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
 | 
			
		||||
	expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
 | 
			
		||||
	failurePolicy := convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policy.Spec.FailurePolicy)
 | 
			
		||||
	var matcher matchconditions.Matcher = nil
 | 
			
		||||
	matchConditions := policy.Spec.MatchConditions
 | 
			
		||||
 | 
			
		||||
	filterCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions)
 | 
			
		||||
	} else {
 | 
			
		||||
		//!TODO: return a validator that always fails with internal error?
 | 
			
		||||
		utilruntime.HandleError(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(matchConditions) > 0 {
 | 
			
		||||
		matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
 | 
			
		||||
		for i := range matchConditions {
 | 
			
		||||
			matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
 | 
			
		||||
		}
 | 
			
		||||
		matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
 | 
			
		||||
	}
 | 
			
		||||
	res := NewValidator(
 | 
			
		||||
		filterCompiler.Compile(convertv1beta1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
 | 
			
		||||
		matcher,
 | 
			
		||||
		filterCompiler.Compile(convertv1beta1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
 | 
			
		||||
		filterCompiler.Compile(convertv1beta1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
 | 
			
		||||
		failurePolicy,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policyType *v1beta1.FailurePolicyType) *v1.FailurePolicyType {
 | 
			
		||||
	if policyType == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var v1FailPolicy v1.FailurePolicyType
 | 
			
		||||
	if *policyType == v1beta1.Fail {
 | 
			
		||||
		v1FailPolicy = v1.Fail
 | 
			
		||||
	} else if *policyType == v1beta1.Ignore {
 | 
			
		||||
		v1FailPolicy = v1.Ignore
 | 
			
		||||
	}
 | 
			
		||||
	return &v1FailPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1Validations(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
 | 
			
		||||
	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
 | 
			
		||||
	for i, validation := range inputValidations {
 | 
			
		||||
		validation := ValidationCondition{
 | 
			
		||||
			Expression: validation.Expression,
 | 
			
		||||
			Message:    validation.Message,
 | 
			
		||||
			Reason:     validation.Reason,
 | 
			
		||||
		}
 | 
			
		||||
		celExpressionAccessor[i] = &validation
 | 
			
		||||
	}
 | 
			
		||||
	return celExpressionAccessor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1MessageExpressions(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
 | 
			
		||||
	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
 | 
			
		||||
	for i, validation := range inputValidations {
 | 
			
		||||
		if validation.MessageExpression != "" {
 | 
			
		||||
			condition := MessageExpressionCondition{
 | 
			
		||||
				MessageExpression: validation.MessageExpression,
 | 
			
		||||
			}
 | 
			
		||||
			celExpressionAccessor[i] = &condition
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return celExpressionAccessor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1AuditAnnotations(inputValidations []v1beta1.AuditAnnotation) []cel.ExpressionAccessor {
 | 
			
		||||
	celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
 | 
			
		||||
	for i, validation := range inputValidations {
 | 
			
		||||
		validation := AuditAnnotationCondition{
 | 
			
		||||
			Key:             validation.Key,
 | 
			
		||||
			ValueExpression: validation.ValueExpression,
 | 
			
		||||
		}
 | 
			
		||||
		celExpressionAccessor[i] = &validation
 | 
			
		||||
	}
 | 
			
		||||
	return celExpressionAccessor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertv1beta1Variables(variables []v1beta1.Variable) []cel.NamedExpressionAccessor {
 | 
			
		||||
	namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
 | 
			
		||||
	for i, variable := range variables {
 | 
			
		||||
		namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
 | 
			
		||||
	}
 | 
			
		||||
	return namedExpressions
 | 
			
		||||
}
 | 
			
		||||
@@ -26,11 +26,35 @@ import (
 | 
			
		||||
	appsv1 "k8s.io/api/apps/v1"
 | 
			
		||||
	corev1 "k8s.io/api/core/v1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/api/meta"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
			
		||||
	"k8s.io/apiserver/pkg/cel/openapi/resolver"
 | 
			
		||||
	"k8s.io/client-go/kubernetes/fake"
 | 
			
		||||
	"k8s.io/kube-openapi/pkg/validation/spec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	scheme *runtime.Scheme = func() *runtime.Scheme {
 | 
			
		||||
		res := runtime.NewScheme()
 | 
			
		||||
		if err := v1beta1.AddToScheme(res); err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := fake.AddToScheme(res); err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return res
 | 
			
		||||
	}()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func must3[T any, I any](val T, _ I, err error) T {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	return val
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestExtractTypeNames(t *testing.T) {
 | 
			
		||||
	for _, tc := range []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user