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

     1  /*
     2  Copyright 2022 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 matching
    18  
    19  import (
    20  	"fmt"
    21  
    22  	v1 "k8s.io/api/admissionregistration/v1"
    23  	corev1 "k8s.io/api/core/v1"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  	"k8s.io/apiserver/pkg/admission"
    26  	"k8s.io/client-go/kubernetes"
    27  	listersv1 "k8s.io/client-go/listers/core/v1"
    28  
    29  	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
    30  	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
    31  	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
    32  )
    33  
    34  type MatchCriteria interface {
    35  	namespace.NamespaceSelectorProvider
    36  	object.ObjectSelectorProvider
    37  
    38  	GetMatchResources() v1.MatchResources
    39  }
    40  
    41  // Matcher decides if a request matches against matchCriteria
    42  type Matcher struct {
    43  	namespaceMatcher *namespace.Matcher
    44  	objectMatcher    *object.Matcher
    45  }
    46  
    47  func (m *Matcher) GetNamespace(name string) (*corev1.Namespace, error) {
    48  	return m.namespaceMatcher.GetNamespace(name)
    49  }
    50  
    51  // NewMatcher initialize the matcher with dependencies requires
    52  func NewMatcher(
    53  	namespaceLister listersv1.NamespaceLister,
    54  	client kubernetes.Interface,
    55  ) *Matcher {
    56  	return &Matcher{
    57  		namespaceMatcher: &namespace.Matcher{
    58  			NamespaceLister: namespaceLister,
    59  			Client:          client,
    60  		},
    61  		objectMatcher: &object.Matcher{},
    62  	}
    63  }
    64  
    65  // ValidateInitialization verify if the matcher is ready before use
    66  func (m *Matcher) ValidateInitialization() error {
    67  	if err := m.namespaceMatcher.Validate(); err != nil {
    68  		return fmt.Errorf("namespaceMatcher is not properly setup: %v", err)
    69  	}
    70  	return nil
    71  }
    72  
    73  func (m *Matcher) Matches(attr admission.Attributes, o admission.ObjectInterfaces, criteria MatchCriteria) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
    74  	matches, matchNsErr := m.namespaceMatcher.MatchNamespaceSelector(criteria, attr)
    75  	// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
    76  	if !matches && matchNsErr == nil {
    77  		return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
    78  	}
    79  
    80  	matches, matchObjErr := m.objectMatcher.MatchObjectSelector(criteria, attr)
    81  	// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
    82  	if !matches && matchObjErr == nil {
    83  		return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
    84  	}
    85  
    86  	matchResources := criteria.GetMatchResources()
    87  	matchPolicy := matchResources.MatchPolicy
    88  	if isExcluded, _, _, err := matchesResourceRules(matchResources.ExcludeResourceRules, matchPolicy, attr, o); isExcluded || err != nil {
    89  		return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, err
    90  	}
    91  
    92  	var (
    93  		isMatch       bool
    94  		matchResource schema.GroupVersionResource
    95  		matchKind     schema.GroupVersionKind
    96  		matchErr      error
    97  	)
    98  	if len(matchResources.ResourceRules) == 0 {
    99  		isMatch = true
   100  		matchKind = attr.GetKind()
   101  		matchResource = attr.GetResource()
   102  	} else {
   103  		isMatch, matchResource, matchKind, matchErr = matchesResourceRules(matchResources.ResourceRules, matchPolicy, attr, o)
   104  	}
   105  	if matchErr != nil {
   106  		return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchErr
   107  	}
   108  	if !isMatch {
   109  		return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
   110  	}
   111  
   112  	// now that we know this applies to this request otherwise, if there were selector errors, return them
   113  	if matchNsErr != nil {
   114  		return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchNsErr
   115  	}
   116  	if matchObjErr != nil {
   117  		return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchObjErr
   118  	}
   119  
   120  	return true, matchResource, matchKind, nil
   121  }
   122  
   123  func matchesResourceRules(namedRules []v1.NamedRuleWithOperations, matchPolicy *v1.MatchPolicyType, attr admission.Attributes, o admission.ObjectInterfaces) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
   124  	matchKind := attr.GetKind()
   125  	matchResource := attr.GetResource()
   126  
   127  	for _, namedRule := range namedRules {
   128  		rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
   129  		ruleMatcher := rules.Matcher{
   130  			Rule: rule,
   131  			Attr: attr,
   132  		}
   133  		if !ruleMatcher.Matches() {
   134  			continue
   135  		}
   136  		// an empty name list always matches
   137  		if len(namedRule.ResourceNames) == 0 {
   138  			return true, matchResource, matchKind, nil
   139  		}
   140  		// TODO: GetName() can return an empty string if the user is relying on
   141  		// the API server to generate the name... figure out what to do for this edge case
   142  		name := attr.GetName()
   143  		for _, matchedName := range namedRule.ResourceNames {
   144  			if name == matchedName {
   145  				return true, matchResource, matchKind, nil
   146  			}
   147  		}
   148  	}
   149  
   150  	// if match policy is undefined or exact, don't perform fuzzy matching
   151  	// note that defaulting to fuzzy matching is set by the API
   152  	if matchPolicy == nil || *matchPolicy == v1.Exact {
   153  		return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
   154  	}
   155  
   156  	attrWithOverride := &attrWithResourceOverride{Attributes: attr}
   157  	equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
   158  	for _, namedRule := range namedRules {
   159  		for _, equivalent := range equivalents {
   160  			if equivalent == attr.GetResource() {
   161  				// we have already checked the original resource
   162  				continue
   163  			}
   164  			attrWithOverride.resource = equivalent
   165  			rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
   166  			m := rules.Matcher{
   167  				Rule: rule,
   168  				Attr: attrWithOverride,
   169  			}
   170  			if !m.Matches() {
   171  				continue
   172  			}
   173  			matchKind = o.GetEquivalentResourceMapper().KindFor(equivalent, attr.GetSubresource())
   174  			if matchKind.Empty() {
   175  				return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("unable to convert to %v: unknown kind", equivalent)
   176  			}
   177  			// an empty name list always matches
   178  			if len(namedRule.ResourceNames) == 0 {
   179  				return true, equivalent, matchKind, nil
   180  			}
   181  
   182  			// TODO: GetName() can return an empty string if the user is relying on
   183  			// the API server to generate the name... figure out what to do for this edge case
   184  			name := attr.GetName()
   185  			for _, matchedName := range namedRule.ResourceNames {
   186  				if name == matchedName {
   187  					return true, equivalent, matchKind, nil
   188  				}
   189  			}
   190  		}
   191  	}
   192  	return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
   193  }
   194  
   195  type attrWithResourceOverride struct {
   196  	admission.Attributes
   197  	resource schema.GroupVersionResource
   198  }
   199  
   200  func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { return a.resource }