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 }