k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/generic/policy_dispatcher.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 generic
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"time"
    24  
    25  	"k8s.io/api/admissionregistration/v1"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	"k8s.io/apimachinery/pkg/api/meta"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    32  	"k8s.io/apiserver/pkg/admission"
    33  	"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
    34  	webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
    35  	"k8s.io/client-go/informers"
    36  	"k8s.io/client-go/tools/cache"
    37  )
    38  
    39  // A policy invocation is a single policy-binding-param tuple from a Policy Hook
    40  // in the context of a specific request. The params have already been resolved
    41  // and any error in configuration or setting up the invocation is stored in
    42  // the Error field.
    43  type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
    44  	// Relevant policy for this hook.
    45  	// This field is always populated
    46  	Policy P
    47  
    48  	// Matched Kind for the request given the policy's matchconstraints
    49  	// May be empty if there was an error matching the resource
    50  	Kind schema.GroupVersionKind
    51  
    52  	// Matched Resource for the request given the policy's matchconstraints
    53  	// May be empty if there was an error matching the resource
    54  	Resource schema.GroupVersionResource
    55  
    56  	// Relevant binding for this hook.
    57  	// May be empty if there was an error with the policy's configuration itself
    58  	Binding B
    59  
    60  	// Compiled policy evaluator
    61  	Evaluator E
    62  
    63  	// Params fetched by the binding to use to evaluate the policy
    64  	Param runtime.Object
    65  
    66  	// Error is set if there was an error with the policy or binding or its
    67  	// params, etc
    68  	Error error
    69  }
    70  
    71  // dispatcherDelegate is called during a request with a pre-filtered list
    72  // of (Policy, Binding, Param) tuples that are active and match the request.
    73  // The dispatcher delegate is responsible for updating the object on the
    74  // admission attributes in the case of mutation, or returning a status error in
    75  // the case of validation.
    76  //
    77  // The delegate provides the "validation" or "mutation" aspect of dispatcher functionality
    78  // (in contrast to generic.PolicyDispatcher which only selects active policies and params)
    79  type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) error
    80  
    81  type policyDispatcher[P runtime.Object, B runtime.Object, E Evaluator] struct {
    82  	newPolicyAccessor  func(P) PolicyAccessor
    83  	newBindingAccessor func(B) BindingAccessor
    84  	matcher            PolicyMatcher
    85  	delegate           dispatcherDelegate[P, B, E]
    86  }
    87  
    88  func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator](
    89  	newPolicyAccessor func(P) PolicyAccessor,
    90  	newBindingAccessor func(B) BindingAccessor,
    91  	matcher *matching.Matcher,
    92  	delegate dispatcherDelegate[P, B, E],
    93  ) Dispatcher[PolicyHook[P, B, E]] {
    94  	return &policyDispatcher[P, B, E]{
    95  		newPolicyAccessor:  newPolicyAccessor,
    96  		newBindingAccessor: newBindingAccessor,
    97  		matcher:            NewPolicyMatcher(matcher),
    98  		delegate:           delegate,
    99  	}
   100  }
   101  
   102  // Dispatch implements generic.Dispatcher. It loops through all active hooks
   103  // (policy x binding pairs) and selects those which are active for the current
   104  // request. It then resolves all params and creates an Invocation for each
   105  // matching policy-binding-param tuple. The delegate is then called with the
   106  // list of tuples.
   107  //
   108  // Note: MatchConditions expressions are not evaluated here. The dispatcher delegate
   109  // is expected to ignore the result of any policies whose match conditions dont pass.
   110  // This may be possible to refactor so matchconditions are checked here instead.
   111  func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook[P, B, E]) error {
   112  	var relevantHooks []PolicyInvocation[P, B, E]
   113  	// Construct all the versions we need to call our webhooks
   114  	versionedAttrAccessor := &versionedAttributeAccessor{
   115  		versionedAttrs:   map[schema.GroupVersionKind]*admission.VersionedAttributes{},
   116  		attr:             a,
   117  		objectInterfaces: o,
   118  	}
   119  
   120  	for _, hook := range hooks {
   121  		policyAccessor := d.newPolicyAccessor(hook.Policy)
   122  		matches, matchGVR, matchGVK, err := d.matcher.DefinitionMatches(a, o, policyAccessor)
   123  		if err != nil {
   124  			// There was an error evaluating if this policy matches anything.
   125  			utilruntime.HandleError(err)
   126  			relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
   127  				Policy: hook.Policy,
   128  				Error:  err,
   129  			})
   130  			continue
   131  		} else if !matches {
   132  			continue
   133  		} else if hook.ConfigurationError != nil {
   134  			// The policy matches but there is a configuration error with the
   135  			// policy itself
   136  			relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
   137  				Policy:   hook.Policy,
   138  				Error:    hook.ConfigurationError,
   139  				Resource: matchGVR,
   140  				Kind:     matchGVK,
   141  			})
   142  			utilruntime.HandleError(hook.ConfigurationError)
   143  			continue
   144  		}
   145  
   146  		for _, binding := range hook.Bindings {
   147  			bindingAccessor := d.newBindingAccessor(binding)
   148  			matches, err = d.matcher.BindingMatches(a, o, bindingAccessor)
   149  			if err != nil {
   150  				// There was an error evaluating if this binding matches anything.
   151  				utilruntime.HandleError(err)
   152  				relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
   153  					Policy:   hook.Policy,
   154  					Binding:  binding,
   155  					Error:    err,
   156  					Resource: matchGVR,
   157  					Kind:     matchGVK,
   158  				})
   159  				continue
   160  			} else if !matches {
   161  				continue
   162  			}
   163  
   164  			// Collect params for this binding
   165  			params, err := CollectParams(
   166  				policyAccessor.GetParamKind(),
   167  				hook.ParamInformer,
   168  				hook.ParamScope,
   169  				bindingAccessor.GetParamRef(),
   170  				a.GetNamespace(),
   171  			)
   172  			if err != nil {
   173  				// There was an error collecting params for this binding.
   174  				utilruntime.HandleError(err)
   175  				relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
   176  					Policy:   hook.Policy,
   177  					Binding:  binding,
   178  					Error:    err,
   179  					Resource: matchGVR,
   180  					Kind:     matchGVK,
   181  				})
   182  				continue
   183  			}
   184  
   185  			// If params is empty and there was no error, that means that
   186  			// ParamNotFoundAction is ignore, so it shouldnt be added to list
   187  			for _, param := range params {
   188  				relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
   189  					Policy:    hook.Policy,
   190  					Binding:   binding,
   191  					Kind:      matchGVK,
   192  					Resource:  matchGVR,
   193  					Param:     param,
   194  					Evaluator: hook.Evaluator,
   195  				})
   196  			}
   197  
   198  			// VersionedAttr result will be cached and reused later during parallel
   199  			// hook calls
   200  			_, err = versionedAttrAccessor.VersionedAttribute(matchGVK)
   201  			if err != nil {
   202  				return apierrors.NewInternalError(err)
   203  			}
   204  		}
   205  
   206  	}
   207  
   208  	if len(relevantHooks) == 0 {
   209  		// no matching hooks
   210  		return nil
   211  	}
   212  
   213  	return d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
   214  }
   215  
   216  // Returns params to use to evaluate a policy-binding with given param
   217  // configuration. If the policy-binding has no param configuration, it
   218  // returns a single-element list with a nil param.
   219  func CollectParams(
   220  	paramKind *v1.ParamKind,
   221  	paramInformer informers.GenericInformer,
   222  	paramScope meta.RESTScope,
   223  	paramRef *v1.ParamRef,
   224  	namespace string,
   225  ) ([]runtime.Object, error) {
   226  	// If definition has paramKind, paramRef is required in binding.
   227  	// If definition has no paramKind, paramRef set in binding will be ignored.
   228  	var params []runtime.Object
   229  	var paramStore cache.GenericNamespaceLister
   230  
   231  	// Make sure the param kind is ready to use
   232  	if paramKind != nil && paramRef != nil {
   233  		if paramInformer == nil {
   234  			return nil, fmt.Errorf("paramKind kind `%v` not known",
   235  				paramKind.String())
   236  		}
   237  
   238  		// Set up cluster-scoped, or namespaced access to the params
   239  		// "default" if not provided, and paramKind is namespaced
   240  		paramStore = paramInformer.Lister()
   241  		if paramScope.Name() == meta.RESTScopeNameNamespace {
   242  			paramsNamespace := namespace
   243  			if len(paramRef.Namespace) > 0 {
   244  				paramsNamespace = paramRef.Namespace
   245  			} else if len(paramsNamespace) == 0 {
   246  				// You must supply namespace if your matcher can possibly
   247  				// match a cluster-scoped resource
   248  				return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
   249  			}
   250  
   251  			paramStore = paramInformer.Lister().ByNamespace(paramsNamespace)
   252  		}
   253  
   254  		// If the param informer for this admission policy has not yet
   255  		// had time to perform an initial listing, don't attempt to use
   256  		// it.
   257  		timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
   258  		defer cancel()
   259  
   260  		if !cache.WaitForCacheSync(timeoutCtx.Done(), paramInformer.Informer().HasSynced) {
   261  			return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
   262  				paramKind.String())
   263  		}
   264  	}
   265  
   266  	// Find params to use with policy
   267  	switch {
   268  	case paramKind == nil:
   269  		// ParamKind is unset. Ignore any globalParamRef or namespaceParamRef
   270  		// setting.
   271  		return []runtime.Object{nil}, nil
   272  	case paramRef == nil:
   273  		// Policy ParamKind is set, but binding does not use it.
   274  		// Validate with nil params
   275  		return []runtime.Object{nil}, nil
   276  	case len(paramRef.Namespace) > 0 && paramScope.Name() == meta.RESTScopeRoot.Name():
   277  		// Not allowed to set namespace for cluster-scoped param
   278  		return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`")
   279  
   280  	case len(paramRef.Name) > 0:
   281  		if paramRef.Selector != nil {
   282  			// This should be validated, but just in case.
   283  			return nil, fmt.Errorf("paramRef.name and paramRef.selector are mutually exclusive")
   284  		}
   285  
   286  		switch param, err := paramStore.Get(paramRef.Name); {
   287  		case err == nil:
   288  			params = []runtime.Object{param}
   289  		case apierrors.IsNotFound(err):
   290  			// Param not yet available. User may need to wait a bit
   291  			// before being able to use it for validation.
   292  			//
   293  			// Set params to nil to prepare for not found action
   294  			params = nil
   295  		case apierrors.IsInvalid(err):
   296  			// Param mis-configured
   297  			// require to set namespace for namespaced resource
   298  			// and unset namespace for cluster scoped resource
   299  			return nil, err
   300  		default:
   301  			// Internal error
   302  			utilruntime.HandleError(err)
   303  			return nil, err
   304  		}
   305  	case paramRef.Selector != nil:
   306  		// Select everything by default if empty name and selector
   307  		selector, err := metav1.LabelSelectorAsSelector(paramRef.Selector)
   308  		if err != nil {
   309  			// Cannot parse label selector: configuration error
   310  			return nil, err
   311  
   312  		}
   313  
   314  		paramList, err := paramStore.List(selector)
   315  		if err != nil {
   316  			// There was a bad internal error
   317  			utilruntime.HandleError(err)
   318  			return nil, err
   319  		}
   320  
   321  		// Successfully grabbed params
   322  		params = paramList
   323  	default:
   324  		// Should be unreachable due to validation
   325  		return nil, fmt.Errorf("one of name or selector must be provided")
   326  	}
   327  
   328  	// Apply fail action for params not found case
   329  	if len(params) == 0 && paramRef.ParameterNotFoundAction != nil && *paramRef.ParameterNotFoundAction == v1.DenyAction {
   330  		return nil, errors.New("no params found for policy binding with `Deny` parameterNotFoundAction")
   331  	}
   332  
   333  	return params, nil
   334  }
   335  
   336  var _ webhookgeneric.VersionedAttributeAccessor = &versionedAttributeAccessor{}
   337  
   338  type versionedAttributeAccessor struct {
   339  	versionedAttrs   map[schema.GroupVersionKind]*admission.VersionedAttributes
   340  	attr             admission.Attributes
   341  	objectInterfaces admission.ObjectInterfaces
   342  }
   343  
   344  func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
   345  	if val, ok := v.versionedAttrs[gvk]; ok {
   346  		return val, nil
   347  	}
   348  	versionedAttr, err := admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  	v.versionedAttrs[gvk] = versionedAttr
   353  	return versionedAttr, nil
   354  }