rebase: update replaced k8s.io modules to v0.33.0

Signed-off-by: Niels de Vos <ndevos@ibm.com>
This commit is contained in:
Niels de Vos
2025-05-07 13:13:33 +02:00
committed by mergify[bot]
parent dd77e72800
commit 107407b44b
1723 changed files with 65035 additions and 175239 deletions

View File

@ -1,155 +0,0 @@
/*
Copyright 2023 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 authorizer
import (
"context"
"encoding/json"
"sort"
"strings"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
type authzResult struct {
authorized authorizer.Decision
reason string
err error
}
type cachingAuthorizer struct {
authorizer authorizer.Authorizer
decisions map[string]authzResult
}
// NewCachingAuthorizer returns an authorizer that caches decisions for the duration
// of the authorizers use. Intended to be used for short-lived operations such as
// the handling of a request in the admission chain, and then discarded.
func NewCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
return &cachingAuthorizer{
authorizer: in,
decisions: make(map[string]authzResult),
}
}
// The attribute accessors known to cache key construction. If this fails to compile, the cache
// implementation may need to be updated.
var _ authorizer.Attributes = (interface {
GetUser() user.Info
GetVerb() string
IsReadOnly() bool
GetNamespace() string
GetResource() string
GetSubresource() string
GetName() string
GetAPIGroup() string
GetAPIVersion() string
IsResourceRequest() bool
GetPath() string
GetFieldSelector() (fields.Requirements, error)
GetLabelSelector() (labels.Requirements, error)
})(nil)
// The user info accessors known to cache key construction. If this fails to compile, the cache
// implementation may need to be updated.
var _ user.Info = (interface {
GetName() string
GetUID() string
GetGroups() []string
GetExtra() map[string][]string
})(nil)
// Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent
// check has already been performed, a cached result is returned. Not safe for concurrent use.
func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
type SerializableAttributes struct {
authorizer.AttributesRecord
LabelSelector string
}
serializableAttributes := SerializableAttributes{
AttributesRecord: authorizer.AttributesRecord{
Verb: a.GetVerb(),
Namespace: a.GetNamespace(),
APIGroup: a.GetAPIGroup(),
APIVersion: a.GetAPIVersion(),
Resource: a.GetResource(),
Subresource: a.GetSubresource(),
Name: a.GetName(),
ResourceRequest: a.IsResourceRequest(),
Path: a.GetPath(),
},
}
// in the error case, we won't honor this field selector, so the cache doesn't need it.
if fieldSelector, err := a.GetFieldSelector(); len(fieldSelector) > 0 {
serializableAttributes.FieldSelectorRequirements, serializableAttributes.FieldSelectorParsingErr = fieldSelector, err
}
if labelSelector, _ := a.GetLabelSelector(); len(labelSelector) > 0 {
// the labels requirements have private elements so those don't help us serialize to a unique key
serializableAttributes.LabelSelector = labelSelector.String()
}
if u := a.GetUser(); u != nil {
di := &user.DefaultInfo{
Name: u.GetName(),
UID: u.GetUID(),
}
// Differently-ordered groups or extras could cause otherwise-equivalent checks to
// have distinct cache keys.
if groups := u.GetGroups(); len(groups) > 0 {
di.Groups = make([]string, len(groups))
copy(di.Groups, groups)
sort.Strings(di.Groups)
}
if extra := u.GetExtra(); len(extra) > 0 {
di.Extra = make(map[string][]string, len(extra))
for k, vs := range extra {
vdupe := make([]string, len(vs))
copy(vdupe, vs)
sort.Strings(vdupe)
di.Extra[k] = vdupe
}
}
serializableAttributes.User = di
}
var b strings.Builder
if err := json.NewEncoder(&b).Encode(serializableAttributes); err != nil {
return authorizer.DecisionNoOpinion, "", err
}
key := b.String()
if cached, ok := ca.decisions[key]; ok {
return cached.authorized, cached.reason, cached.err
}
authorized, reason, err := ca.authorizer.Authorize(ctx, a)
ca.decisions[key] = authzResult{
authorized: authorized,
reason: reason,
err: err,
}
return authorized, reason, err
}

View File

@ -1,233 +0,0 @@
/*
Copyright 2015 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 lifecycle
import (
"context"
"fmt"
"io"
"time"
"k8s.io/klog/v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
utilcache "k8s.io/apimachinery/pkg/util/cache"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
corelisters "k8s.io/client-go/listers/core/v1"
"k8s.io/utils/clock"
)
const (
// PluginName indicates the name of admission plug-in
PluginName = "NamespaceLifecycle"
// how long a namespace stays in the force live lookup cache before expiration.
forceLiveLookupTTL = 30 * time.Second
// how long to wait for a missing namespace before re-checking the cache (and then doing a live lookup)
// this accomplishes two things:
// 1. It allows a watch-fed cache time to observe a namespace creation event
// 2. It allows time for a namespace creation to distribute to members of a storage cluster,
// so the live lookup has a better chance of succeeding even if it isn't performed against the leader.
missingNamespaceWait = 50 * time.Millisecond
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
return NewLifecycle(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem, metav1.NamespacePublic))
})
}
// Lifecycle is an implementation of admission.Interface.
// It enforces life-cycle constraints around a Namespace depending on its Phase
type Lifecycle struct {
*admission.Handler
client kubernetes.Interface
immortalNamespaces sets.String
namespaceLister corelisters.NamespaceLister
// forceLiveLookupCache holds a list of entries for namespaces that we have a strong reason to believe are stale in our local cache.
// if a namespace is in this cache, then we will ignore our local state and always fetch latest from api server.
forceLiveLookupCache *utilcache.LRUExpireCache
}
var _ = initializer.WantsExternalKubeInformerFactory(&Lifecycle{})
var _ = initializer.WantsExternalKubeClientSet(&Lifecycle{})
// Admit makes an admission decision based on the request attributes
func (l *Lifecycle) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
// prevent deletion of immortal namespaces
if a.GetOperation() == admission.Delete && a.GetKind().GroupKind() == v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() && l.immortalNamespaces.Has(a.GetName()) {
return errors.NewForbidden(a.GetResource().GroupResource(), a.GetName(), fmt.Errorf("this namespace may not be deleted"))
}
// always allow non-namespaced resources
if len(a.GetNamespace()) == 0 && a.GetKind().GroupKind() != v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() {
return nil
}
if a.GetKind().GroupKind() == v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() {
// if a namespace is deleted, we want to prevent all further creates into it
// while it is undergoing termination. to reduce incidences where the cache
// is slow to update, we add the namespace into a force live lookup list to ensure
// we are not looking at stale state.
if a.GetOperation() == admission.Delete {
l.forceLiveLookupCache.Add(a.GetName(), true, forceLiveLookupTTL)
}
// allow all operations to namespaces
return nil
}
// always allow deletion of other resources
if a.GetOperation() == admission.Delete {
return nil
}
// always allow access review checks. Returning status about the namespace would be leaking information
if isAccessReview(a) {
return nil
}
// we need to wait for our caches to warm
if !l.WaitForReady() {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
var (
exists bool
err error
)
namespace, err := l.namespaceLister.Get(a.GetNamespace())
if err != nil {
if !errors.IsNotFound(err) {
return errors.NewInternalError(err)
}
} else {
exists = true
}
if !exists && a.GetOperation() == admission.Create {
// give the cache time to observe the namespace before rejecting a create.
// this helps when creating a namespace and immediately creating objects within it.
time.Sleep(missingNamespaceWait)
namespace, err = l.namespaceLister.Get(a.GetNamespace())
switch {
case errors.IsNotFound(err):
// no-op
case err != nil:
return errors.NewInternalError(err)
default:
exists = true
}
if exists {
klog.V(4).InfoS("Namespace existed in cache after waiting", "namespace", klog.KRef("", a.GetNamespace()))
}
}
// forceLiveLookup if true will skip looking at local cache state and instead always make a live call to server.
forceLiveLookup := false
if _, ok := l.forceLiveLookupCache.Get(a.GetNamespace()); ok {
// we think the namespace was marked for deletion, but our current local cache says otherwise, we will force a live lookup.
forceLiveLookup = exists && namespace.Status.Phase == v1.NamespaceActive
}
// refuse to operate on non-existent namespaces
if !exists || forceLiveLookup {
// as a last resort, make a call directly to storage
namespace, err = l.client.CoreV1().Namespaces().Get(context.TODO(), a.GetNamespace(), metav1.GetOptions{})
switch {
case errors.IsNotFound(err):
return err
case err != nil:
return errors.NewInternalError(err)
}
klog.V(4).InfoS("Found namespace via storage lookup", "namespace", klog.KRef("", a.GetNamespace()))
}
// ensure that we're not trying to create objects in terminating namespaces
if a.GetOperation() == admission.Create {
if namespace.Status.Phase != v1.NamespaceTerminating {
return nil
}
err := admission.NewForbidden(a, fmt.Errorf("unable to create new content in namespace %s because it is being terminated", a.GetNamespace()))
if apierr, ok := err.(*errors.StatusError); ok {
apierr.ErrStatus.Details.Causes = append(apierr.ErrStatus.Details.Causes, metav1.StatusCause{
Type: v1.NamespaceTerminatingCause,
Message: fmt.Sprintf("namespace %s is being terminated", a.GetNamespace()),
Field: "metadata.namespace",
})
}
return err
}
return nil
}
// NewLifecycle creates a new namespace Lifecycle admission control handler
func NewLifecycle(immortalNamespaces sets.String) (*Lifecycle, error) {
return newLifecycleWithClock(immortalNamespaces, clock.RealClock{})
}
func newLifecycleWithClock(immortalNamespaces sets.String, clock utilcache.Clock) (*Lifecycle, error) {
forceLiveLookupCache := utilcache.NewLRUExpireCacheWithClock(100, clock)
return &Lifecycle{
Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete),
immortalNamespaces: immortalNamespaces,
forceLiveLookupCache: forceLiveLookupCache,
}, nil
}
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
func (l *Lifecycle) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
namespaceInformer := f.Core().V1().Namespaces()
l.namespaceLister = namespaceInformer.Lister()
l.SetReadyFunc(namespaceInformer.Informer().HasSynced)
}
// SetExternalKubeClientSet implements the WantsExternalKubeClientSet interface.
func (l *Lifecycle) SetExternalKubeClientSet(client kubernetes.Interface) {
l.client = client
}
// ValidateInitialization implements the InitializationValidator interface.
func (l *Lifecycle) ValidateInitialization() error {
if l.namespaceLister == nil {
return fmt.Errorf("missing namespaceLister")
}
if l.client == nil {
return fmt.Errorf("missing client")
}
return nil
}
// accessReviewResources are resources which give a view into permissions in a namespace. Users must be allowed to create these
// resources because returning "not found" errors allows someone to search for the "people I'm going to fire in 2017" namespace.
var accessReviewResources = map[schema.GroupResource]bool{
{Group: "authorization.k8s.io", Resource: "localsubjectaccessreviews"}: true,
}
func isAccessReview(a admission.Attributes) bool {
return accessReviewResources[a.GetResource().GroupResource()]
}

View File

@ -1,43 +0,0 @@
/*
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 generic
import (
"k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/types"
)
type PolicyAccessor interface {
GetName() string
GetNamespace() string
GetParamKind() *v1.ParamKind
GetMatchConstraints() *v1.MatchResources
GetFailurePolicy() *v1.FailurePolicyType
}
type BindingAccessor interface {
GetName() string
GetNamespace() string
// GetPolicyName returns the name of the (Validating/Mutating)AdmissionPolicy,
// which is cluster-scoped, so namespace is usually left blank.
// But we leave the door open to add a namespaced vesion in the future
GetPolicyName() types.NamespacedName
GetParamRef() *v1.ParamRef
GetMatchResources() *v1.MatchResources
}

View File

@ -1,67 +0,0 @@
/*
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 generic
import (
"context"
"k8s.io/apiserver/pkg/admission"
)
// Hook represents a dynamic admission hook. The hook may be a webhook or a
// policy. For webhook, the Hook may describe how to contact the endpoint, expected
// cert, etc. For policies, the hook may describe a compiled policy-binding pair.
type Hook interface {
// All hooks are expected to contain zero or more match conditions, object
// selectors, namespace selectors to help the dispatcher decide when to apply
// the hook.
//
// Methods of matching logic is applied are specific to the hook and left up
// to the implementation.
}
// Source can list dynamic admission plugins.
type Source[H Hook] interface {
// Hooks returns the list of currently known admission hooks.
Hooks() []H
// Run the source. This method should be called only once at startup.
Run(ctx context.Context) error
// HasSynced returns true if the source has completed its initial sync.
HasSynced() bool
}
// Dispatcher dispatches evaluates an admission request against the currently
// active hooks returned by the source.
type Dispatcher[H Hook] interface {
// Start the dispatcher. This method should be called only once at startup.
Start(ctx context.Context) error
// Dispatch a request to the policies. Dispatcher may choose not to
// call a hook, either because the rules of the hook does not match, or
// the namespaceSelector or the objectSelector of the hook does not
// match. A non-nil error means the request is rejected.
Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []H) error
}
// An evaluator represents a compiled CEL expression that can be evaluated a
// given a set of inputs used by the generic PolicyHook for Mutating and
// ValidatingAdmissionPolicy.
// Mutating and Validating may have different forms of evaluators
type Evaluator interface {
}

View File

@ -1,221 +0,0 @@
/*
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 generic
import (
"context"
"errors"
"fmt"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
)
// H is the Hook type generated by the source and consumed by the dispatcher.
// !TODO: Just pass in a Plugin[H] with accessors to all this information
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) Dispatcher[H]
// admissionResources is the list of resources related to CEL-based admission
// features.
var admissionResources = []schema.GroupResource{
{Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicies"},
{Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicybindings"},
{Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicies"},
{Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicybindings"},
}
// AdmissionPolicyManager is an abstract admission plugin with all the
// infrastructure to define Admit or Validate on-top.
type Plugin[H any] struct {
*admission.Handler
sourceFactory sourceFactory[H]
dispatcherFactory dispatcherFactory[H]
source Source[H]
dispatcher Dispatcher[H]
matcher *matching.Matcher
informerFactory informers.SharedInformerFactory
client kubernetes.Interface
restMapper meta.RESTMapper
dynamicClient dynamic.Interface
excludedResources sets.Set[schema.GroupResource]
stopCh <-chan struct{}
authorizer authorizer.Authorizer
enabled bool
}
var (
_ initializer.WantsExternalKubeInformerFactory = &Plugin[any]{}
_ initializer.WantsExternalKubeClientSet = &Plugin[any]{}
_ initializer.WantsRESTMapper = &Plugin[any]{}
_ initializer.WantsDynamicClient = &Plugin[any]{}
_ initializer.WantsDrainedNotification = &Plugin[any]{}
_ initializer.WantsAuthorizer = &Plugin[any]{}
_ initializer.WantsExcludedAdmissionResources = &Plugin[any]{}
_ admission.InitializationValidator = &Plugin[any]{}
)
func NewPlugin[H any](
handler *admission.Handler,
sourceFactory sourceFactory[H],
dispatcherFactory dispatcherFactory[H],
) *Plugin[H] {
return &Plugin[H]{
Handler: handler,
sourceFactory: sourceFactory,
dispatcherFactory: dispatcherFactory,
// always exclude admission/mutating policies and bindings
excludedResources: sets.New(admissionResources...),
}
}
func (c *Plugin[H]) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
c.informerFactory = f
}
func (c *Plugin[H]) SetExternalKubeClientSet(client kubernetes.Interface) {
c.client = client
}
func (c *Plugin[H]) SetRESTMapper(mapper meta.RESTMapper) {
c.restMapper = mapper
}
func (c *Plugin[H]) SetDynamicClient(client dynamic.Interface) {
c.dynamicClient = client
}
func (c *Plugin[H]) SetDrainedNotification(stopCh <-chan struct{}) {
c.stopCh = stopCh
}
func (c *Plugin[H]) SetAuthorizer(authorizer authorizer.Authorizer) {
c.authorizer = authorizer
}
func (c *Plugin[H]) SetMatcher(matcher *matching.Matcher) {
c.matcher = matcher
}
func (c *Plugin[H]) SetEnabled(enabled bool) {
c.enabled = enabled
}
func (c *Plugin[H]) SetExcludedAdmissionResources(excludedResources []schema.GroupResource) {
c.excludedResources.Insert(excludedResources...)
}
// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
func (c *Plugin[H]) ValidateInitialization() error {
// By default enabled is set to false. It is up to types which embed this
// struct to set it to true (if feature gate is enabled, or other conditions)
if !c.enabled {
return nil
}
if c.Handler == nil {
return errors.New("missing handler")
}
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")
}
// Use default matcher
namespaceInformer := c.informerFactory.Core().V1().Namespaces()
c.matcher = matching.NewMatcher(namespaceInformer.Lister(), c.client)
if err := c.matcher.ValidateInitialization(); err != nil {
return err
}
c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher, c.client)
pluginContext, pluginContextCancel := context.WithCancel(context.Background())
go func() {
defer pluginContextCancel()
<-c.stopCh
}()
go func() {
err := c.source.Run(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %w", err))
}
}()
err := c.dispatcher.Start(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err))
}
c.SetReadyFunc(func() bool {
return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
})
return nil
}
func (c *Plugin[H]) Dispatch(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
) (err error) {
if !c.enabled {
return nil
} else if c.shouldIgnoreResource(a) {
return nil
} else if !c.WaitForReady() {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
return c.dispatcher.Dispatch(ctx, a, o, c.source.Hooks())
}
func (c *Plugin[H]) shouldIgnoreResource(attr admission.Attributes) bool {
gvr := attr.GetResource()
// exclusion decision ignores the version.
gr := gvr.GroupResource()
return c.excludedResources.Has(gr)
}

View File

@ -1,417 +0,0 @@
/*
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 generic
import (
"context"
"errors"
"fmt"
"time"
"k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
)
// PolicyInvocation is a single policy-binding-param tuple from a Policy Hook
// in the context of a specific request. The params have already been resolved
// and any error in configuration or setting up the invocation is stored in
// the Error field.
type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
// Relevant policy for this hook.
// This field is always populated
Policy P
// Matched Kind for the request given the policy's matchconstraints
// May be empty if there was an error matching the resource
Kind schema.GroupVersionKind
// Matched Resource for the request given the policy's matchconstraints
// May be empty if there was an error matching the resource
Resource schema.GroupVersionResource
// Relevant binding for this hook.
// May be empty if there was an error with the policy's configuration itself
Binding B
// Compiled policy evaluator
Evaluator E
// Params fetched by the binding to use to evaluate the policy
Param runtime.Object
}
// dispatcherDelegate is called during a request with a pre-filtered list
// of (Policy, Binding, Param) tuples that are active and match the request.
// The dispatcher delegate is responsible for updating the object on the
// admission attributes in the case of mutation, or returning a status error in
// the case of validation.
//
// The delegate provides the "validation" or "mutation" aspect of dispatcher functionality
// (in contrast to generic.PolicyDispatcher which only selects active policies and params)
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]) ([]PolicyError, *apierrors.StatusError)
type policyDispatcher[P runtime.Object, B runtime.Object, E Evaluator] struct {
newPolicyAccessor func(P) PolicyAccessor
newBindingAccessor func(B) BindingAccessor
matcher PolicyMatcher
delegate dispatcherDelegate[P, B, E]
}
func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator](
newPolicyAccessor func(P) PolicyAccessor,
newBindingAccessor func(B) BindingAccessor,
matcher *matching.Matcher,
delegate dispatcherDelegate[P, B, E],
) Dispatcher[PolicyHook[P, B, E]] {
return &policyDispatcher[P, B, E]{
newPolicyAccessor: newPolicyAccessor,
newBindingAccessor: newBindingAccessor,
matcher: NewPolicyMatcher(matcher),
delegate: delegate,
}
}
// Dispatch implements generic.Dispatcher. It loops through all active hooks
// (policy x binding pairs) and selects those which are active for the current
// request. It then resolves all params and creates an Invocation for each
// matching policy-binding-param tuple. The delegate is then called with the
// list of tuples.
func (d *policyDispatcher[P, B, E]) Start(ctx context.Context) error {
return nil
}
// Note: MatchConditions expressions are not evaluated here. The dispatcher delegate
// is expected to ignore the result of any policies whose match conditions dont pass.
// This may be possible to refactor so matchconditions are checked here instead.
func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook[P, B, E]) error {
var relevantHooks []PolicyInvocation[P, B, E]
// Construct all the versions we need to call our webhooks
versionedAttrAccessor := &versionedAttributeAccessor{
versionedAttrs: map[schema.GroupVersionKind]*admission.VersionedAttributes{},
attr: a,
objectInterfaces: o,
}
var policyErrors []PolicyError
addConfigError := func(err error, definition PolicyAccessor, binding BindingAccessor) {
var message error
if binding == nil {
message = fmt.Errorf("failed to configure policy: %w", err)
} else {
message = fmt.Errorf("failed to configure binding: %w", err)
}
policyErrors = append(policyErrors, PolicyError{
Policy: definition,
Binding: binding,
Message: message,
})
}
for _, hook := range hooks {
policyAccessor := d.newPolicyAccessor(hook.Policy)
matches, matchGVR, matchGVK, err := d.matcher.DefinitionMatches(a, o, policyAccessor)
if err != nil {
// There was an error evaluating if this policy matches anything.
addConfigError(err, policyAccessor, nil)
continue
} else if !matches {
continue
} else if hook.ConfigurationError != nil {
addConfigError(hook.ConfigurationError, policyAccessor, nil)
continue
}
for _, binding := range hook.Bindings {
bindingAccessor := d.newBindingAccessor(binding)
matches, err = d.matcher.BindingMatches(a, o, bindingAccessor)
if err != nil {
// There was an error evaluating if this binding matches anything.
addConfigError(err, policyAccessor, bindingAccessor)
continue
} else if !matches {
continue
}
// here the binding matches.
// VersionedAttr result will be cached and reused later during parallel
// hook calls.
if _, err = versionedAttrAccessor.VersionedAttribute(matchGVK); err != nil {
// VersionedAttr result will be cached and reused later during parallel
// hook calls.
addConfigError(err, policyAccessor, nil)
continue
}
// Collect params for this binding
params, err := CollectParams(
policyAccessor.GetParamKind(),
hook.ParamInformer,
hook.ParamScope,
bindingAccessor.GetParamRef(),
a.GetNamespace(),
)
if err != nil {
// There was an error collecting params for this binding.
addConfigError(err, policyAccessor, bindingAccessor)
continue
}
// If params is empty and there was no error, that means that
// ParamNotFoundAction is ignore, so it shouldnt be added to list
for _, param := range params {
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Kind: matchGVK,
Resource: matchGVR,
Param: param,
Evaluator: hook.Evaluator,
})
}
}
}
if len(relevantHooks) > 0 {
extraPolicyErrors, statusError := d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
if statusError != nil {
return statusError
}
policyErrors = append(policyErrors, extraPolicyErrors...)
}
var filteredErrors []PolicyError
for _, e := range policyErrors {
// we always default the FailurePolicy if it is unset and validate it in API level
var policy v1.FailurePolicyType
if fp := e.Policy.GetFailurePolicy(); fp == nil {
policy = v1.Fail
} else {
policy = *fp
}
switch policy {
case v1.Ignore:
// TODO: add metrics for ignored error here
continue
case v1.Fail:
filteredErrors = append(filteredErrors, e)
default:
filteredErrors = append(filteredErrors, e)
}
}
if len(filteredErrors) > 0 {
forbiddenErr := admission.NewForbidden(a, fmt.Errorf("admission request denied by policy"))
// The forbiddenErr is always a StatusError.
var err *apierrors.StatusError
if !errors.As(forbiddenErr, &err) {
// Should never happen.
return apierrors.NewInternalError(fmt.Errorf("failed to create status error"))
}
err.ErrStatus.Message = ""
for _, policyError := range filteredErrors {
message := policyError.Error()
// If this is the first denied decision, use its message and reason
// for the status error message.
if err.ErrStatus.Message == "" {
err.ErrStatus.Message = message
if policyError.Reason != "" {
err.ErrStatus.Reason = policyError.Reason
}
}
// Add the denied decision's message to the status error's details
err.ErrStatus.Details.Causes = append(
err.ErrStatus.Details.Causes,
metav1.StatusCause{Message: message})
}
return err
}
return nil
}
// Returns params to use to evaluate a policy-binding with given param
// configuration. If the policy-binding has no param configuration, it
// returns a single-element list with a nil param.
func CollectParams(
paramKind *v1.ParamKind,
paramInformer informers.GenericInformer,
paramScope meta.RESTScope,
paramRef *v1.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 cache.GenericNamespaceLister
// Make sure the param kind is ready to use
if paramKind != nil && paramRef != 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 = paramInformer.Lister()
if paramScope.Name() == meta.RESTScopeNameNamespace {
paramsNamespace := namespace
if len(paramRef.Namespace) > 0 {
paramsNamespace = paramRef.Namespace
} else if len(paramsNamespace) == 0 {
// You must supply namespace if your matcher can possibly
// match a cluster-scoped resource
return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
}
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(context.Background(), 1*time.Second)
defer cancel()
if !cache.WaitForCacheSync(timeoutCtx.Done(), paramInformer.Informer().HasSynced) {
return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
paramKind.String())
}
}
// Find params to use with policy
switch {
case paramKind == nil:
// ParamKind is unset. Ignore any globalParamRef or namespaceParamRef
// setting.
return []runtime.Object{nil}, nil
case paramRef == nil:
// Policy ParamKind is set, but binding does not use it.
// Validate with nil params
return []runtime.Object{nil}, nil
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`")
case len(paramRef.Name) > 0:
if paramRef.Selector != nil {
// This should be validated, but just in case.
return nil, fmt.Errorf("paramRef.name and paramRef.selector are mutually exclusive")
}
switch param, err := paramStore.Get(paramRef.Name); {
case err == nil:
params = []runtime.Object{param}
case apierrors.IsNotFound(err):
// Param not yet available. User may need to wait a bit
// before being able to use it for validation.
//
// Set params to nil to prepare for not found action
params = nil
case apierrors.IsInvalid(err):
// Param mis-configured
// require to set namespace for namespaced resource
// and unset namespace for cluster scoped resource
return nil, err
default:
// Internal error
utilruntime.HandleError(err)
return nil, err
}
case paramRef.Selector != nil:
// Select everything by default if empty name and selector
selector, err := metav1.LabelSelectorAsSelector(paramRef.Selector)
if err != nil {
// Cannot parse label selector: configuration error
return nil, err
}
paramList, err := paramStore.List(selector)
if err != nil {
// There was a bad internal error
utilruntime.HandleError(err)
return nil, err
}
// Successfully grabbed params
params = paramList
default:
// Should be unreachable due to validation
return nil, fmt.Errorf("one of name or selector must be provided")
}
// Apply fail action for params not found case
if len(params) == 0 && paramRef.ParameterNotFoundAction != nil && *paramRef.ParameterNotFoundAction == v1.DenyAction {
return nil, errors.New("no params found for policy binding with `Deny` parameterNotFoundAction")
}
return params, nil
}
var _ webhookgeneric.VersionedAttributeAccessor = &versionedAttributeAccessor{}
type versionedAttributeAccessor struct {
versionedAttrs map[schema.GroupVersionKind]*admission.VersionedAttributes
attr admission.Attributes
objectInterfaces admission.ObjectInterfaces
}
func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
if val, ok := v.versionedAttrs[gvk]; ok {
return val, nil
}
versionedAttr, err := admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces)
if err != nil {
return nil, err
}
v.versionedAttrs[gvk] = versionedAttr
return versionedAttr, nil
}
type PolicyError struct {
Policy PolicyAccessor
Binding BindingAccessor
Message error
Reason metav1.StatusReason
}
func (c PolicyError) Error() string {
if c.Binding != nil {
return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error())
}
return fmt.Sprintf("policy %q denied request: %s", c.Policy.GetName(), c.Message.Error())
}

View File

@ -1,108 +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 generic
import (
"fmt"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
)
// Matcher is used for matching ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding to attributes
type PolicyMatcher interface {
admission.InitializationValidator
// DefinitionMatches says whether this policy definition matches the provided admission
// resource request
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition PolicyAccessor) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error)
// BindingMatches says whether this policy definition matches the provided admission
// resource request
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding BindingAccessor) (bool, error)
// GetNamespace retrieves the Namespace resource by the given name. The name may be empty, in which case
// GetNamespace must return nil, nil
GetNamespace(name string) (*corev1.Namespace, error)
}
type matcher struct {
Matcher *matching.Matcher
}
func NewPolicyMatcher(m *matching.Matcher) PolicyMatcher {
return &matcher{
Matcher: m,
}
}
// ValidateInitialization checks if Matcher is initialized.
func (c *matcher) ValidateInitialization() error {
return c.Matcher.ValidateInitialization()
}
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition PolicyAccessor) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
constraints := definition.GetMatchConstraints()
if constraints == nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("policy contained no match constraints, a required field")
}
criteria := matchCriteria{constraints: constraints}
return c.Matcher.Matches(a, o, &criteria)
}
// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request
func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding BindingAccessor) (bool, error) {
matchResources := binding.GetMatchResources()
if matchResources == nil {
return true, nil
}
criteria := matchCriteria{constraints: matchResources}
isMatch, _, _, err := c.Matcher.Matches(a, o, &criteria)
return isMatch, err
}
func (c *matcher) GetNamespace(name string) (*corev1.Namespace, error) {
return c.Matcher.GetNamespace(name)
}
var _ matching.MatchCriteria = &matchCriteria{}
type matchCriteria struct {
constraints *admissionregistrationv1.MatchResources
}
// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector)
}
// GetParsedObjectSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedObjectSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.ObjectSelector)
}
// GetMatchResources returns the matchConstraints
func (m *matchCriteria) GetMatchResources() admissionregistrationv1.MatchResources {
return *m.constraints
}

View File

@ -1,493 +0,0 @@
/*
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 generic
import (
"context"
goerrors "errors"
"fmt"
"sync"
"sync/atomic"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
)
// Interval for refreshing policies.
// TODO: Consider reducing this to a shorter duration or replacing this entirely
// with checks that detect when a policy change took effect.
const policyRefreshIntervalDefault = 1 * time.Second
var policyRefreshInterval = policyRefreshIntervalDefault
type policySource[P runtime.Object, B runtime.Object, E Evaluator] struct {
ctx context.Context
policyInformer generic.Informer[P]
bindingInformer generic.Informer[B]
restMapper meta.RESTMapper
newPolicyAccessor func(P) PolicyAccessor
newBindingAccessor func(B) BindingAccessor
informerFactory informers.SharedInformerFactory
dynamicClient dynamic.Interface
compiler func(P) E
// Currently compiled list of valid/active policy-binding pairs
policies atomic.Pointer[[]PolicyHook[P, B, E]]
// Whether the cache of policies is dirty and needs to be recompiled
policiesDirty atomic.Bool
lock sync.Mutex
compiledPolicies map[types.NamespacedName]compiledPolicyEntry[E]
// Temporary until we use the dynamic informer factory
paramsCRDControllers map[schema.GroupVersionKind]*paramInfo
}
type paramInfo struct {
mapping meta.RESTMapping
// When the param is changed, or the informer is done being used, the cancel
// func should be called to stop/cleanup the original informer
cancelFunc func()
// The lister for this param
informer informers.GenericInformer
}
type compiledPolicyEntry[E Evaluator] struct {
policyVersion string
evaluator E
}
type PolicyHook[P runtime.Object, B runtime.Object, E Evaluator] struct {
Policy P
Bindings []B
// ParamInformer is the informer for the param CRD for this policy, or nil if
// there is no param or if there was a configuration error
ParamInformer informers.GenericInformer
ParamScope meta.RESTScope
Evaluator E
ConfigurationError error
}
var _ Source[PolicyHook[runtime.Object, runtime.Object, Evaluator]] = &policySource[runtime.Object, runtime.Object, Evaluator]{}
func NewPolicySource[P runtime.Object, B runtime.Object, E Evaluator](
policyInformer cache.SharedIndexInformer,
bindingInformer cache.SharedIndexInformer,
newPolicyAccessor func(P) PolicyAccessor,
newBindingAccessor func(B) BindingAccessor,
compiler func(P) E,
paramInformerFactory informers.SharedInformerFactory,
dynamicClient dynamic.Interface,
restMapper meta.RESTMapper,
) Source[PolicyHook[P, B, E]] {
res := &policySource[P, B, E]{
compiler: compiler,
policyInformer: generic.NewInformer[P](policyInformer),
bindingInformer: generic.NewInformer[B](bindingInformer),
compiledPolicies: map[types.NamespacedName]compiledPolicyEntry[E]{},
newPolicyAccessor: newPolicyAccessor,
newBindingAccessor: newBindingAccessor,
paramsCRDControllers: map[schema.GroupVersionKind]*paramInfo{},
informerFactory: paramInformerFactory,
dynamicClient: dynamicClient,
restMapper: restMapper,
}
return res
}
// SetPolicyRefreshIntervalForTests allows the refresh interval to be overridden during tests.
// This should only be called from tests.
func SetPolicyRefreshIntervalForTests(interval time.Duration) func() {
policyRefreshInterval = interval
return func() {
policyRefreshInterval = policyRefreshIntervalDefault
}
}
func (s *policySource[P, B, E]) Run(ctx context.Context) error {
if s.ctx != nil {
return fmt.Errorf("policy source already running")
}
// Wait for initial cache sync of policies and informers before reconciling
// any
if !cache.WaitForNamedCacheSync(fmt.Sprintf("%T", s), ctx.Done(), s.UpstreamHasSynced) {
err := ctx.Err()
if err == nil {
err = fmt.Errorf("initial cache sync for %T failed", s)
}
return err
}
s.ctx = ctx
// Perform initial policy compilation after initial list has finished
s.notify()
s.refreshPolicies()
notifyFuncs := cache.ResourceEventHandlerFuncs{
AddFunc: func(_ interface{}) {
s.notify()
},
UpdateFunc: func(_, _ interface{}) {
s.notify()
},
DeleteFunc: func(_ interface{}) {
s.notify()
},
}
handle, err := s.policyInformer.AddEventHandler(notifyFuncs)
if err != nil {
return err
}
defer func() {
if err := s.policyInformer.RemoveEventHandler(handle); err != nil {
utilruntime.HandleError(fmt.Errorf("failed to remove policy event handler: %w", err))
}
}()
bindingHandle, err := s.bindingInformer.AddEventHandler(notifyFuncs)
if err != nil {
return err
}
defer func() {
if err := s.bindingInformer.RemoveEventHandler(bindingHandle); err != nil {
utilruntime.HandleError(fmt.Errorf("failed to remove binding event handler: %w", err))
}
}()
// Start a worker that checks every second to see if policy data is dirty
// and needs to be recompiled
go func() {
// Loop every 1 second until context is cancelled, refreshing policies
wait.Until(s.refreshPolicies, policyRefreshInterval, ctx.Done())
}()
<-ctx.Done()
return nil
}
func (s *policySource[P, B, E]) UpstreamHasSynced() bool {
return s.policyInformer.HasSynced() && s.bindingInformer.HasSynced()
}
// HasSynced implements Source.
func (s *policySource[P, B, E]) HasSynced() bool {
// As an invariant we never store `nil` into the atomic list of processed
// policy hooks. If it is nil, then we haven't compiled all the policies
// and stored them yet.
return s.Hooks() != nil
}
// Hooks implements Source.
func (s *policySource[P, B, E]) Hooks() []PolicyHook[P, B, E] {
res := s.policies.Load()
// Error case should not happen since evaluation function never
// returns error
if res == nil {
// Not yet synced
return nil
}
return *res
}
func (s *policySource[P, B, E]) refreshPolicies() {
if !s.UpstreamHasSynced() {
return
} else if !s.policiesDirty.Swap(false) {
return
}
// It is ok the cache gets marked dirty again between us clearing the
// flag and us calculating the policies. The dirty flag would be marked again,
// and we'd have a no-op after comparing resource versions on the next sync.
klog.Infof("refreshing policies")
policies, err := s.calculatePolicyData()
// Intentionally store policy list regardless of error. There may be
// an error returned if there was a configuration error in one of the policies,
// but we would still want those policies evaluated
// (for instance to return error on failaction). Or if there was an error
// listing all policies at all, we would want to wipe the list.
s.policies.Store(&policies)
if err != nil {
// An error was generated while syncing policies. Mark it as dirty again
// so we can retry later
utilruntime.HandleError(fmt.Errorf("encountered error syncing policies: %w. Rescheduling policy sync", err))
s.notify()
}
}
func (s *policySource[P, B, E]) notify() {
s.policiesDirty.Store(true)
}
// calculatePolicyData calculates the list of policies and bindings for each
// policy. If there is an error in generation, it will return the error and
// the partial list of policies that were able to be generated. Policies that
// have an error will have a non-nil ConfigurationError field, but still be
// included in the result.
//
// This function caches the result of the intermediate compilations
func (s *policySource[P, B, E]) calculatePolicyData() ([]PolicyHook[P, B, E], error) {
if !s.UpstreamHasSynced() {
return nil, fmt.Errorf("cannot calculate policy data until upstream has synced")
}
// Fat-fingered lock that can be made more fine-tuned if required
s.lock.Lock()
defer s.lock.Unlock()
// Create a local copy of all policies and bindings
policiesToBindings := map[types.NamespacedName][]B{}
bindingList, err := s.bindingInformer.List(labels.Everything())
if err != nil {
// This should never happen unless types are misconfigured
// (can't use meta.accessor on them)
return nil, err
}
// Gather a list of all active policy bindings
for _, bindingSpec := range bindingList {
bindingAccessor := s.newBindingAccessor(bindingSpec)
policyKey := bindingAccessor.GetPolicyName()
// Add this binding to the list of bindings for this policy
policiesToBindings[policyKey] = append(policiesToBindings[policyKey], bindingSpec)
}
result := make([]PolicyHook[P, B, E], 0, len(bindingList))
usedParams := map[schema.GroupVersionKind]struct{}{}
var errs []error
for policyKey, bindingSpecs := range policiesToBindings {
var inf generic.NamespacedLister[P] = s.policyInformer
if len(policyKey.Namespace) > 0 {
inf = s.policyInformer.Namespaced(policyKey.Namespace)
}
policySpec, err := inf.Get(policyKey.Name)
if errors.IsNotFound(err) {
// Policy for bindings doesn't exist. This can happen if the policy
// was deleted before the binding, or the binding was created first.
//
// Just skip bindings that refer to non-existent policies
// If the policy is recreated, the cache will be marked dirty and
// this function will run again.
continue
} else if err != nil {
// This should never happen since fetching from a cache should never
// fail and this function checks that the cache was synced before
// even getting to this point.
errs = append(errs, err)
continue
}
var parsedParamKind *schema.GroupVersionKind
policyAccessor := s.newPolicyAccessor(policySpec)
if paramKind := policyAccessor.GetParamKind(); paramKind != nil {
groupVersion, err := schema.ParseGroupVersion(paramKind.APIVersion)
if err != nil {
errs = append(errs, fmt.Errorf("failed to parse paramKind APIVersion: %w", err))
continue
}
parsedParamKind = &schema.GroupVersionKind{
Group: groupVersion.Group,
Version: groupVersion.Version,
Kind: paramKind.Kind,
}
// TEMPORARY UNTIL WE HAVE SHARED PARAM INFORMERS
usedParams[*parsedParamKind] = struct{}{}
}
paramInformer, paramScope, configurationError := s.ensureParamsForPolicyLocked(parsedParamKind)
result = append(result, PolicyHook[P, B, E]{
Policy: policySpec,
Bindings: bindingSpecs,
Evaluator: s.compilePolicyLocked(policySpec),
ParamInformer: paramInformer,
ParamScope: paramScope,
ConfigurationError: configurationError,
})
// Should queue a re-sync for policy sync error. If our shared param
// informer can notify us when CRD discovery changes we can remove this
// and just rely on the informer to notify us when the CRDs change
if configurationError != nil {
errs = append(errs, configurationError)
}
}
// Clean up orphaned policies by replacing the old cache of compiled policies
// (the map of used policies is updated by `compilePolicy`)
for policyKey := range s.compiledPolicies {
if _, wasSeen := policiesToBindings[policyKey]; !wasSeen {
delete(s.compiledPolicies, policyKey)
}
}
// Clean up orphaned param informers
for paramKind, info := range s.paramsCRDControllers {
if _, wasSeen := usedParams[paramKind]; !wasSeen {
info.cancelFunc()
delete(s.paramsCRDControllers, paramKind)
}
}
err = nil
if len(errs) > 0 {
err = goerrors.Join(errs...)
}
return result, err
}
// ensureParamsForPolicyLocked ensures that the informer for the paramKind is
// started and returns the informer and the scope of the paramKind.
//
// Must be called under write lock
func (s *policySource[P, B, E]) ensureParamsForPolicyLocked(paramSource *schema.GroupVersionKind) (informers.GenericInformer, meta.RESTScope, error) {
if paramSource == nil {
return nil, nil, nil
} else if info, ok := s.paramsCRDControllers[*paramSource]; ok {
return info.informer, info.mapping.Scope, nil
}
mapping, err := s.restMapper.RESTMapping(schema.GroupKind{
Group: paramSource.Group,
Kind: paramSource.Kind,
}, paramSource.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
return nil, nil, fmt.Errorf("failed to find resource referenced by paramKind: '%v'", *paramSource)
}
// We are not watching this param. Start an informer for it.
instanceContext, instanceCancel := context.WithCancel(s.ctx)
var informer informers.GenericInformer
// 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 := s.informerFactory.ForResource(mapping.Resource); err == nil {
informer = genericInformer
// Start the informer
s.informerFactory.Start(instanceContext.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(
s.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,
)
go informer.Informer().Run(instanceContext.Done())
}
klog.Infof("informer started for %v", *paramSource)
ret := &paramInfo{
mapping: *mapping,
cancelFunc: instanceCancel,
informer: informer,
}
s.paramsCRDControllers[*paramSource] = ret
return ret.informer, mapping.Scope, nil
}
// For testing
func (s *policySource[P, B, E]) getParamInformer(param schema.GroupVersionKind) (informers.GenericInformer, meta.RESTScope) {
s.lock.Lock()
defer s.lock.Unlock()
if info, ok := s.paramsCRDControllers[param]; ok {
return info.informer, info.mapping.Scope
}
return nil, nil
}
// compilePolicyLocked compiles the policy and returns the evaluator for it.
// If the policy has not changed since the last compilation, it will return
// the cached evaluator.
//
// Must be called under write lock
func (s *policySource[P, B, E]) compilePolicyLocked(policySpec P) E {
policyMeta, err := meta.Accessor(policySpec)
if err != nil {
// This should not happen if P, and B have ObjectMeta, but
// unfortunately there is no way to express "able to call
// meta.Accessor" as a type constraint
utilruntime.HandleError(err)
var emptyEvaluator E
return emptyEvaluator
}
key := types.NamespacedName{
Namespace: policyMeta.GetNamespace(),
Name: policyMeta.GetName(),
}
compiledPolicy, wasCompiled := s.compiledPolicies[key]
// If the policy or binding has changed since it was last compiled,
// and if there is no configuration error (like a missing param CRD)
// then we recompile
if !wasCompiled ||
compiledPolicy.policyVersion != policyMeta.GetResourceVersion() {
compiledPolicy = compiledPolicyEntry[E]{
policyVersion: policyMeta.GetResourceVersion(),
evaluator: s.compiler(policySpec),
}
s.compiledPolicies[key] = compiledPolicy
}
return compiledPolicy.evaluator
}

View File

@ -1,626 +0,0 @@
/*
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 generic
import (
"context"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
"k8s.io/component-base/featuregate"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// PolicyTestContext is everything you need to unit test a policy plugin
type PolicyTestContext[P runtime.Object, B runtime.Object, E Evaluator] struct {
context.Context
Plugin *Plugin[PolicyHook[P, B, E]]
Source Source[PolicyHook[P, B, E]]
Start func() error
scheme *runtime.Scheme
restMapper *meta.DefaultRESTMapper
policyGVR schema.GroupVersionResource
bindingGVR schema.GroupVersionResource
policyGVK schema.GroupVersionKind
bindingGVK schema.GroupVersionKind
nativeTracker clienttesting.ObjectTracker
policyAndBindingTracker clienttesting.ObjectTracker
unstructuredTracker clienttesting.ObjectTracker
}
func NewPolicyTestContext[P, B runtime.Object, E Evaluator](
newPolicyAccessor func(P) PolicyAccessor,
newBindingAccessor func(B) BindingAccessor,
compileFunc func(P) E,
dispatcher dispatcherFactory[PolicyHook[P, B, E]],
initialObjects []runtime.Object,
paramMappings []meta.RESTMapping,
) (*PolicyTestContext[P, B, E], func(), error) {
var Pexample P
var Bexample B
// Create a fake resource and kind for the provided policy and binding types
fakePolicyGVR := schema.GroupVersionResource{
Group: "policy.example.com",
Version: "v1",
Resource: "fakepolicies",
}
fakeBindingGVR := schema.GroupVersionResource{
Group: "policy.example.com",
Version: "v1",
Resource: "fakebindings",
}
fakePolicyGVK := fakePolicyGVR.GroupVersion().WithKind("FakePolicy")
fakeBindingGVK := fakeBindingGVR.GroupVersion().WithKind("FakeBinding")
policySourceTestScheme, err := func() (*runtime.Scheme, error) {
scheme := runtime.NewScheme()
if err := fake.AddToScheme(scheme); err != nil {
return nil, err
}
scheme.AddKnownTypeWithName(fakePolicyGVK, Pexample)
scheme.AddKnownTypeWithName(fakeBindingGVK, Bexample)
scheme.AddKnownTypeWithName(fakePolicyGVK.GroupVersion().WithKind(fakePolicyGVK.Kind+"List"), &FakeList[P]{})
scheme.AddKnownTypeWithName(fakeBindingGVK.GroupVersion().WithKind(fakeBindingGVK.Kind+"List"), &FakeList[B]{})
for _, mapping := range paramMappings {
// Skip if it is in the scheme already
if scheme.Recognizes(mapping.GroupVersionKind) {
continue
}
scheme.AddKnownTypeWithName(mapping.GroupVersionKind, &unstructured.Unstructured{})
scheme.AddKnownTypeWithName(mapping.GroupVersionKind.GroupVersion().WithKind(mapping.GroupVersionKind.Kind+"List"), &unstructured.UnstructuredList{})
}
return scheme, nil
}()
if err != nil {
return nil, nil, err
}
fakeRestMapper := func() *meta.DefaultRESTMapper {
res := meta.NewDefaultRESTMapper([]schema.GroupVersion{
{
Group: "",
Version: "v1",
},
})
res.Add(fakePolicyGVK, meta.RESTScopeRoot)
res.Add(fakeBindingGVK, meta.RESTScopeRoot)
res.Add(corev1.SchemeGroupVersion.WithKind("ConfigMap"), meta.RESTScopeNamespace)
for _, mapping := range paramMappings {
res.AddSpecific(mapping.GroupVersionKind, mapping.Resource, mapping.Resource, mapping.Scope)
}
return res
}()
nativeClient := fake.NewSimpleClientset()
dynamicClient := dynamicfake.NewSimpleDynamicClient(policySourceTestScheme)
fakeInformerFactory := informers.NewSharedInformerFactory(nativeClient, 30*time.Second)
// Make an object tracker specifically for our policies and bindings
policiesAndBindingsTracker := clienttesting.NewObjectTracker(
policySourceTestScheme,
serializer.NewCodecFactory(policySourceTestScheme).UniversalDecoder())
// Make an informer for our policies and bindings
policyInformer := cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return policiesAndBindingsTracker.List(fakePolicyGVR, fakePolicyGVK, "")
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return policiesAndBindingsTracker.Watch(fakePolicyGVR, "")
},
},
Pexample,
30*time.Second,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
)
bindingInformer := cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return policiesAndBindingsTracker.List(fakeBindingGVR, fakeBindingGVK, "")
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return policiesAndBindingsTracker.Watch(fakeBindingGVR, "")
},
},
Bexample,
30*time.Second,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
)
var source Source[PolicyHook[P, B, E]]
plugin := NewPlugin[PolicyHook[P, B, E]](
admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update),
func(sif informers.SharedInformerFactory, i1 kubernetes.Interface, i2 dynamic.Interface, r meta.RESTMapper) Source[PolicyHook[P, B, E]] {
source = NewPolicySource[P, B, E](
policyInformer,
bindingInformer,
newPolicyAccessor,
newBindingAccessor,
compileFunc,
sif,
i2,
r,
)
return source
}, dispatcher)
plugin.SetEnabled(true)
featureGate := featuregate.NewFeatureGate()
testContext, testCancel := context.WithCancel(context.Background())
genericInitializer := initializer.New(
nativeClient,
dynamicClient,
fakeInformerFactory,
fakeAuthorizer{},
featureGate,
testContext.Done(),
fakeRestMapper,
)
genericInitializer.Initialize(plugin)
plugin.SetRESTMapper(fakeRestMapper)
if err := plugin.ValidateInitialization(); err != nil {
testCancel()
return nil, nil, err
}
res := &PolicyTestContext[P, B, E]{
Context: testContext,
Plugin: plugin,
Source: source,
restMapper: fakeRestMapper,
scheme: policySourceTestScheme,
policyGVK: fakePolicyGVK,
bindingGVK: fakeBindingGVK,
policyGVR: fakePolicyGVR,
bindingGVR: fakeBindingGVR,
nativeTracker: nativeClient.Tracker(),
policyAndBindingTracker: policiesAndBindingsTracker,
unstructuredTracker: dynamicClient.Tracker(),
}
for _, obj := range initialObjects {
err := res.updateOne(obj)
if err != nil {
testCancel()
return nil, nil, err
}
}
res.Start = func() error {
fakeInformerFactory.Start(res.Done())
go policyInformer.Run(res.Done())
go bindingInformer.Run(res.Done())
if !cache.WaitForCacheSync(res.Done(), res.Source.HasSynced) {
return fmt.Errorf("timed out waiting for initial cache sync")
}
return nil
}
return res, testCancel, nil
}
// UpdateAndWait updates the given object in the test, or creates it if it doesn't exist
// Depending upon object type, waits afterward until the object is synced
// by the policy source
//
// Be aware the UpdateAndWait will modify the ResourceVersion of the
// provided objects.
func (p *PolicyTestContext[P, B, E]) UpdateAndWait(objects ...runtime.Object) error {
return p.update(true, objects...)
}
// Update updates the given object in the test, or creates it if it doesn't exist
//
// Be aware the Update will modify the ResourceVersion of the
// provided objects.
func (p *PolicyTestContext[P, B, E]) Update(objects ...runtime.Object) error {
return p.update(false, objects...)
}
// Objects the given object in the test, or creates it if it doesn't exist
// Depending upon object type, waits afterward until the object is synced
// by the policy source
func (p *PolicyTestContext[P, B, E]) update(wait bool, objects ...runtime.Object) error {
for _, object := range objects {
if err := p.updateOne(object); err != nil {
return err
}
}
if wait {
timeoutCtx, timeoutCancel := context.WithTimeout(p, 3*time.Second)
defer timeoutCancel()
for _, object := range objects {
if err := p.WaitForReconcile(timeoutCtx, object); err != nil {
return fmt.Errorf("error waiting for reconcile of %v: %v", object, err)
}
}
}
return nil
}
// Depending upon object type, waits afterward until the object is synced
// by the policy source. Note that policies that are not bound are skipped,
// so you should not try to wait for an unbound policy. Create both the binding
// and policy, then wait.
func (p *PolicyTestContext[P, B, E]) WaitForReconcile(timeoutCtx context.Context, object runtime.Object) error {
if !p.Source.HasSynced() {
return nil
}
objectMeta, err := meta.Accessor(object)
if err != nil {
return err
}
objectGVK, _, err := p.inferGVK(object)
if err != nil {
return err
}
switch objectGVK {
case p.policyGVK:
return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
policies := p.Source.Hooks()
for _, policy := range policies {
policyMeta, err := meta.Accessor(policy.Policy)
if err != nil {
return true, err
} else if policyMeta.GetName() == objectMeta.GetName() && policyMeta.GetResourceVersion() == objectMeta.GetResourceVersion() {
return true, nil
}
}
return false, nil
})
case p.bindingGVK:
return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
policies := p.Source.Hooks()
for _, policy := range policies {
for _, binding := range policy.Bindings {
bindingMeta, err := meta.Accessor(binding)
if err != nil {
return true, err
} else if bindingMeta.GetName() == objectMeta.GetName() && bindingMeta.GetResourceVersion() == objectMeta.GetResourceVersion() {
return true, nil
}
}
}
return false, nil
})
default:
// Do nothing, params are visible immediately
// Loop until one of the params is visible via get of the param informer
return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
informer, scope := p.Source.(*policySource[P, B, E]).getParamInformer(objectGVK)
if informer == nil {
// Informer does not exist yet, keep waiting for sync
return false, nil
}
if !cache.WaitForCacheSync(timeoutCtx.Done(), informer.Informer().HasSynced) {
return false, fmt.Errorf("timed out waiting for cache sync of param informer")
}
var lister cache.GenericNamespaceLister = informer.Lister()
if scope == meta.RESTScopeNamespace {
lister = informer.Lister().ByNamespace(objectMeta.GetNamespace())
}
fetched, err := lister.Get(objectMeta.GetName())
if err != nil {
if errors.IsNotFound(err) {
return false, nil
}
return true, err
}
// Ensure RV matches
fetchedMeta, err := meta.Accessor(fetched)
if err != nil {
return true, err
} else if fetchedMeta.GetResourceVersion() != objectMeta.GetResourceVersion() {
return false, nil
}
return true, nil
})
}
}
func (p *PolicyTestContext[P, B, E]) waitForDelete(ctx context.Context, objectGVK schema.GroupVersionKind, name types.NamespacedName) error {
srce := p.Source.(*policySource[P, B, E])
return wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
switch objectGVK {
case p.policyGVK:
for _, hook := range p.Source.Hooks() {
accessor := srce.newPolicyAccessor(hook.Policy)
if accessor.GetName() == name.Name && accessor.GetNamespace() == name.Namespace {
return false, nil
}
}
return true, nil
case p.bindingGVK:
for _, hook := range p.Source.Hooks() {
for _, binding := range hook.Bindings {
accessor := srce.newBindingAccessor(binding)
if accessor.GetName() == name.Name && accessor.GetNamespace() == name.Namespace {
return false, nil
}
}
}
return true, nil
default:
// Do nothing, params are visible immediately
// Loop until one of the params is visible via get of the param informer
informer, scope := p.Source.(*policySource[P, B, E]).getParamInformer(objectGVK)
if informer == nil {
return true, nil
}
var lister cache.GenericNamespaceLister = informer.Lister()
if scope == meta.RESTScopeNamespace {
lister = informer.Lister().ByNamespace(name.Namespace)
}
_, err = lister.Get(name.Name)
if err != nil {
if errors.IsNotFound(err) {
return true, nil
}
return false, err
}
return false, nil
}
})
}
func (p *PolicyTestContext[P, B, E]) updateOne(object runtime.Object) error {
objectMeta, err := meta.Accessor(object)
if err != nil {
return err
}
objectMeta.SetResourceVersion(string(uuid.NewUUID()))
objectGVK, gvr, err := p.inferGVK(object)
if err != nil {
return err
}
switch objectGVK {
case p.policyGVK:
err := p.policyAndBindingTracker.Update(p.policyGVR, object, objectMeta.GetNamespace())
if errors.IsNotFound(err) {
err = p.policyAndBindingTracker.Create(p.policyGVR, object, objectMeta.GetNamespace())
}
return err
case p.bindingGVK:
err := p.policyAndBindingTracker.Update(p.bindingGVR, object, objectMeta.GetNamespace())
if errors.IsNotFound(err) {
err = p.policyAndBindingTracker.Create(p.bindingGVR, object, objectMeta.GetNamespace())
}
return err
default:
if _, ok := object.(*unstructured.Unstructured); ok {
if err := p.unstructuredTracker.Create(gvr, object, objectMeta.GetNamespace()); err != nil {
if errors.IsAlreadyExists(err) {
return p.unstructuredTracker.Update(gvr, object, objectMeta.GetNamespace())
}
return err
}
return nil
} else if err := p.nativeTracker.Create(gvr, object, objectMeta.GetNamespace()); err != nil {
if errors.IsAlreadyExists(err) {
return p.nativeTracker.Update(gvr, object, objectMeta.GetNamespace())
}
}
return nil
}
}
// Depending upon object type, waits afterward until the object is synced
// by the policy source
func (p *PolicyTestContext[P, B, E]) DeleteAndWait(object ...runtime.Object) error {
for _, object := range object {
if err := p.deleteOne(object); err != nil && !errors.IsNotFound(err) {
return err
}
}
timeoutCtx, timeoutCancel := context.WithTimeout(p, 3*time.Second)
defer timeoutCancel()
for _, object := range object {
accessor, err := meta.Accessor(object)
if err != nil {
return err
}
objectGVK, _, err := p.inferGVK(object)
if err != nil {
return err
}
if err := p.waitForDelete(
timeoutCtx,
objectGVK,
types.NamespacedName{Name: accessor.GetName(), Namespace: accessor.GetNamespace()}); err != nil {
return err
}
}
return nil
}
func (p *PolicyTestContext[P, B, E]) deleteOne(object runtime.Object) error {
objectMeta, err := meta.Accessor(object)
if err != nil {
return err
}
objectMeta.SetResourceVersion(string(uuid.NewUUID()))
objectGVK, gvr, err := p.inferGVK(object)
if err != nil {
return err
}
switch objectGVK {
case p.policyGVK:
return p.policyAndBindingTracker.Delete(p.policyGVR, objectMeta.GetNamespace(), objectMeta.GetName())
case p.bindingGVK:
return p.policyAndBindingTracker.Delete(p.bindingGVR, objectMeta.GetNamespace(), objectMeta.GetName())
default:
if _, ok := object.(*unstructured.Unstructured); ok {
return p.unstructuredTracker.Delete(gvr, objectMeta.GetNamespace(), objectMeta.GetName())
}
return p.nativeTracker.Delete(gvr, objectMeta.GetNamespace(), objectMeta.GetName())
}
}
func (p *PolicyTestContext[P, B, E]) Dispatch(
new, old runtime.Object,
operation admission.Operation,
) error {
if old == nil && new == nil {
return fmt.Errorf("both old and new objects cannot be nil")
}
nonNilObject := new
if nonNilObject == nil {
nonNilObject = old
}
gvk, gvr, err := p.inferGVK(nonNilObject)
if err != nil {
return err
}
nonNilMeta, err := meta.Accessor(nonNilObject)
if err != nil {
return err
}
return p.Plugin.Dispatch(
p,
admission.NewAttributesRecord(
new,
old,
gvk,
nonNilMeta.GetName(),
nonNilMeta.GetNamespace(),
gvr,
"",
operation,
nil,
false,
nil,
), admission.NewObjectInterfacesFromScheme(p.scheme))
}
func (p *PolicyTestContext[P, B, E]) inferGVK(object runtime.Object) (schema.GroupVersionKind, schema.GroupVersionResource, error) {
objectGVK := object.GetObjectKind().GroupVersionKind()
if objectGVK.Empty() {
// If the object doesn't have a GVK, ask the schema for preferred GVK
knownKinds, _, err := p.scheme.ObjectKinds(object)
if err != nil {
return schema.GroupVersionKind{}, schema.GroupVersionResource{}, err
} else if len(knownKinds) == 0 {
return schema.GroupVersionKind{}, schema.GroupVersionResource{}, fmt.Errorf("no known GVKs for object in schema: %T", object)
}
toTake := 0
// Prefer GVK if it is our fake policy or binding
for i, knownKind := range knownKinds {
if knownKind == p.policyGVK || knownKind == p.bindingGVK {
toTake = i
break
}
}
objectGVK = knownKinds[toTake]
}
// Make sure GVK is known to the fake rest mapper. To prevent cryptic error
mapping, err := p.restMapper.RESTMapping(objectGVK.GroupKind(), objectGVK.Version)
if err != nil {
return schema.GroupVersionKind{}, schema.GroupVersionResource{}, err
}
return objectGVK, mapping.Resource, nil
}
type FakeList[T runtime.Object] struct {
metav1.TypeMeta
metav1.ListMeta
Items []T
}
func (fl *FakeList[P]) DeepCopyObject() runtime.Object {
copiedItems := make([]P, len(fl.Items))
for i, item := range fl.Items {
copiedItems[i] = item.DeepCopyObject().(P)
}
return &FakeList[P]{
TypeMeta: fl.TypeMeta,
ListMeta: fl.ListMeta,
Items: copiedItems,
}
}
type fakeAuthorizer struct{}
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
return authorizer.DecisionAllow, "", nil
}

View File

@ -1,283 +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 generic
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/cache/synctrack"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
var _ Controller[runtime.Object] = &controller[runtime.Object]{}
type controller[T runtime.Object] struct {
informer Informer[T]
queue workqueue.TypedRateLimitingInterface[string]
// Returns an error if there was a transient error during reconciliation
// and the object should be tried again later.
reconciler func(namespace, name string, newObj T) error
options ControllerOptions
// must hold a func() bool or nil
notificationsDelivered atomic.Value
hasProcessed synctrack.AsyncTracker[string]
}
type ControllerOptions struct {
Name string
Workers uint
}
func (c *controller[T]) Informer() Informer[T] {
return c.informer
}
func NewController[T runtime.Object](
informer Informer[T],
reconciler func(namepace, name string, newObj T) error,
options ControllerOptions,
) Controller[T] {
if options.Workers == 0 {
options.Workers = 2
}
if len(options.Name) == 0 {
options.Name = fmt.Sprintf("%T-controller", *new(T))
}
c := &controller[T]{
options: options,
informer: informer,
reconciler: reconciler,
queue: nil,
}
c.hasProcessed.UpstreamHasSynced = func() bool {
f := c.notificationsDelivered.Load()
if f == nil {
return false
}
return f.(func() bool)()
}
return c
}
// Runs the controller and returns an error explaining why running was stopped.
// Reconciliation ends as soon as the context completes. If there are events
// waiting to be processed at that itme, they will be dropped.
func (c *controller[T]) Run(ctx context.Context) error {
klog.Infof("starting %s", c.options.Name)
defer klog.Infof("stopping %s", c.options.Name)
c.queue = workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{Name: c.options.Name},
)
// Forcefully shutdown workqueue. Drop any enqueued items.
// Important to do this in a `defer` at the start of `Run`.
// Otherwise, if there are any early returns without calling this, we
// would never shut down the workqueue
defer c.queue.ShutDown()
enqueue := func(obj interface{}, isInInitialList bool) {
var key string
var err error
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
utilruntime.HandleError(err)
return
}
if isInInitialList {
c.hasProcessed.Start(key)
}
c.queue.Add(key)
}
registration, err := c.informer.AddEventHandler(cache.ResourceEventHandlerDetailedFuncs{
AddFunc: enqueue,
UpdateFunc: func(oldObj, newObj interface{}) {
oldMeta, err1 := meta.Accessor(oldObj)
newMeta, err2 := meta.Accessor(newObj)
if err1 != nil || err2 != nil {
if err1 != nil {
utilruntime.HandleError(err1)
}
if err2 != nil {
utilruntime.HandleError(err2)
}
return
} else if oldMeta.GetResourceVersion() == newMeta.GetResourceVersion() {
if len(oldMeta.GetResourceVersion()) == 0 {
klog.Warningf("%v throwing out update with empty RV. this is likely to happen if a test did not supply a resource version on an updated object", c.options.Name)
}
return
}
enqueue(newObj, false)
},
DeleteFunc: func(obj interface{}) {
// Enqueue
enqueue(obj, false)
},
})
// Error might be raised if informer was started and stopped already
if err != nil {
return err
}
c.notificationsDelivered.Store(registration.HasSynced)
// Make sure event handler is removed from informer in case return early from
// an error
defer func() {
c.notificationsDelivered.Store(func() bool { return false })
// Remove event handler and Handle Error here. Error should only be raised
// for improper usage of event handler API.
if err := c.informer.RemoveEventHandler(registration); err != nil {
utilruntime.HandleError(err)
}
}()
// Wait for initial cache list to complete before beginning to reconcile
// objects.
if !cache.WaitForNamedCacheSync(c.options.Name, ctx.Done(), c.informer.HasSynced) {
// ctx cancelled during cache sync. return early
err := ctx.Err()
if err == nil {
// if context wasnt cancelled then the sync failed for another reason
err = errors.New("cache sync failed")
}
return err
}
waitGroup := sync.WaitGroup{}
for i := uint(0); i < c.options.Workers; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
wait.Until(c.runWorker, time.Second, ctx.Done())
}()
}
klog.Infof("Started %v workers for %v", c.options.Workers, c.options.Name)
// Wait for context cancel.
<-ctx.Done()
// Forcefully shutdown workqueue. Drop any enqueued items.
c.queue.ShutDown()
// Workqueue shutdown signals for workers to stop. Wait for all workers to
// clean up
waitGroup.Wait()
// Only way for workers to ever stop is for caller to cancel the context
return ctx.Err()
}
func (c *controller[T]) HasSynced() bool {
return c.hasProcessed.HasSynced()
}
func (c *controller[T]) runWorker() {
for {
key, shutdown := c.queue.Get()
if shutdown {
return
}
// We wrap this block in a func so we can defer c.workqueue.Done.
err := func(obj string) error {
// We call Done here so the workqueue knows we have finished
// processing this item. We also must remember to call Forget if we
// do not want this work item being re-queued. For example, we do
// not call Forget if a transient error occurs, instead the item is
// put back on the workqueue and attempted again after a back-off
// period.
defer c.queue.Done(obj)
defer c.hasProcessed.Finished(key)
if err := c.reconcile(key); err != nil {
// Put the item back on the workqueue to handle any transient errors.
c.queue.AddRateLimited(key)
return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
}
// Finally, if no error occurs we Forget this item so it is allowed
// to be re-enqueued without a long rate limit
c.queue.Forget(obj)
klog.V(4).Infof("syncAdmissionPolicy(%q)", key)
return nil
}(key)
if err != nil {
utilruntime.HandleError(err)
}
}
}
func (c *controller[T]) reconcile(key string) error {
var newObj T
var err error
var namespace string
var name string
var lister NamespacedLister[T]
// Convert the namespace/name string into a distinct namespace and name
namespace, name, err = cache.SplitMetaNamespaceKey(key)
if err != nil {
utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return nil
}
if len(namespace) > 0 {
lister = c.informer.Namespaced(namespace)
} else {
lister = c.informer
}
newObj, err = lister.Get(name)
if err != nil {
if !kerrors.IsNotFound(err) {
return err
}
// Deleted object. Inform reconciler with empty
}
return c.reconciler(namespace, name, newObj)
}

View File

@ -1,29 +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 generic contains a typed wrapper over cache SharedIndexInformer
// and Lister (maybe eventually should have a home there?)
//
// This interface is being experimented with as an easier way to write controllers
// with a bit less boilerplate.
//
// Informer/Lister classes are thin wrappers providing a type-safe interface
// over regular interface{}-based Informers/Listers
//
// Controller[T] provides a reusable way to reconcile objects out of an informer
// using the tried and true controller design pattern found all over k8s
// codebase based upon syncFunc/reconcile
package generic

View File

@ -1,40 +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 generic
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
var _ Informer[runtime.Object] = informer[runtime.Object]{}
type informer[T runtime.Object] struct {
cache.SharedIndexInformer
lister[T]
}
// Creates a generic informer around a type-erased cache.SharedIndexInformer
// It is incumbent on the caller to ensure that the generic type argument is
// consistent with the type of the objects stored inside the SharedIndexInformer
// as they will be casted.
func NewInformer[T runtime.Object](informe cache.SharedIndexInformer) Informer[T] {
return informer[T]{
SharedIndexInformer: informe,
lister: NewLister[T](informe.GetIndexer()),
}
}

View File

@ -1,62 +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 generic
import (
"context"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
type Controller[T runtime.Object] interface {
// Meant to be run inside a goroutine
// Waits for and reacts to changes in whatever type the controller
// is concerned with.
//
// Returns an error always non-nil explaining why the worker stopped
Run(ctx context.Context) error
// Retrieves the informer used to back this controller
Informer() Informer[T]
// Returns true if the informer cache has synced, and all the objects from
// the initial list have been reconciled at least once.
HasSynced() bool
}
type NamespacedLister[T any] interface {
// List lists all ValidationRuleSets in the indexer for a given namespace.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []T, err error)
// Get retrieves the ValidationRuleSet from the indexer for a given namespace and name.
// Objects returned here must be treated as read-only.
Get(name string) (T, error)
}
type Informer[T any] interface {
cache.SharedIndexInformer
Lister[T]
}
// Lister[T] helps list Ts.
// All objects returned here must be treated as read-only.
type Lister[T any] interface {
NamespacedLister[T]
Namespaced(namespace string) NamespacedLister[T]
}

View File

@ -1,100 +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 generic
import (
"fmt"
"net/http"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
var _ Lister[runtime.Object] = lister[runtime.Object]{}
type namespacedLister[T runtime.Object] struct {
indexer cache.Indexer
namespace string
}
func (w namespacedLister[T]) List(selector labels.Selector) (ret []T, err error) {
err = cache.ListAllByNamespace(w.indexer, w.namespace, selector, func(m interface{}) {
ret = append(ret, m.(T))
})
return ret, err
}
func (w namespacedLister[T]) Get(name string) (T, error) {
var result T
obj, exists, err := w.indexer.GetByKey(w.namespace + "/" + name)
if err != nil {
return result, err
}
if !exists {
return result, &kerrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotFound,
Reason: metav1.StatusReasonNotFound,
Message: fmt.Sprintf("%s not found", name),
}}
}
result = obj.(T)
return result, nil
}
type lister[T runtime.Object] struct {
indexer cache.Indexer
}
func (w lister[T]) List(selector labels.Selector) (ret []T, err error) {
err = cache.ListAll(w.indexer, selector, func(m interface{}) {
ret = append(ret, m.(T))
})
return ret, err
}
func (w lister[T]) Get(name string) (T, error) {
var result T
obj, exists, err := w.indexer.GetByKey(name)
if err != nil {
return result, err
}
if !exists {
// kerrors.StatusNotFound requires a GroupResource we cannot provide
return result, &kerrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotFound,
Reason: metav1.StatusReasonNotFound,
Message: fmt.Sprintf("%s not found", name),
}}
}
result = obj.(T)
return result, nil
}
func (w lister[T]) Namespaced(namespace string) NamespacedLister[T] {
return namespacedLister[T]{namespace: namespace, indexer: w.indexer}
}
func NewLister[T runtime.Object](indexer cache.Indexer) lister[T] {
return lister[T]{indexer: indexer}
}

View File

@ -1,200 +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 matching
import (
"fmt"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/kubernetes"
listersv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
)
type MatchCriteria interface {
namespace.NamespaceSelectorProvider
object.ObjectSelectorProvider
GetMatchResources() v1.MatchResources
}
// Matcher decides if a request matches against matchCriteria
type Matcher struct {
namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
}
func (m *Matcher) GetNamespace(name string) (*corev1.Namespace, error) {
return m.namespaceMatcher.GetNamespace(name)
}
// NewMatcher initialize the matcher with dependencies requires
func NewMatcher(
namespaceLister listersv1.NamespaceLister,
client kubernetes.Interface,
) *Matcher {
return &Matcher{
namespaceMatcher: &namespace.Matcher{
NamespaceLister: namespaceLister,
Client: client,
},
objectMatcher: &object.Matcher{},
}
}
// ValidateInitialization verify if the matcher is ready before use
func (m *Matcher) ValidateInitialization() error {
if err := m.namespaceMatcher.Validate(); err != nil {
return fmt.Errorf("namespaceMatcher is not properly setup: %v", err)
}
return nil
}
func (m *Matcher) Matches(attr admission.Attributes, o admission.ObjectInterfaces, criteria MatchCriteria) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
matches, matchNsErr := m.namespaceMatcher.MatchNamespaceSelector(criteria, attr)
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
if !matches && matchNsErr == nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
matches, matchObjErr := m.objectMatcher.MatchObjectSelector(criteria, attr)
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
if !matches && matchObjErr == nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
matchResources := criteria.GetMatchResources()
matchPolicy := matchResources.MatchPolicy
if isExcluded, _, _, err := matchesResourceRules(matchResources.ExcludeResourceRules, matchPolicy, attr, o); isExcluded || err != nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, err
}
var (
isMatch bool
matchResource schema.GroupVersionResource
matchKind schema.GroupVersionKind
matchErr error
)
if len(matchResources.ResourceRules) == 0 {
isMatch = true
matchKind = attr.GetKind()
matchResource = attr.GetResource()
} else {
isMatch, matchResource, matchKind, matchErr = matchesResourceRules(matchResources.ResourceRules, matchPolicy, attr, o)
}
if matchErr != nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchErr
}
if !isMatch {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
// now that we know this applies to this request otherwise, if there were selector errors, return them
if matchNsErr != nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchNsErr
}
if matchObjErr != nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchObjErr
}
return true, matchResource, matchKind, nil
}
func matchesResourceRules(namedRules []v1.NamedRuleWithOperations, matchPolicy *v1.MatchPolicyType, attr admission.Attributes, o admission.ObjectInterfaces) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
matchKind := attr.GetKind()
matchResource := attr.GetResource()
for _, namedRule := range namedRules {
rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
ruleMatcher := rules.Matcher{
Rule: rule,
Attr: attr,
}
if !ruleMatcher.Matches() {
continue
}
// an empty name list always matches
if len(namedRule.ResourceNames) == 0 {
return true, matchResource, matchKind, nil
}
// TODO: GetName() can return an empty string if the user is relying on
// the API server to generate the name... figure out what to do for this edge case
name := attr.GetName()
for _, matchedName := range namedRule.ResourceNames {
if name == matchedName {
return true, matchResource, matchKind, nil
}
}
}
// if match policy is undefined or exact, don't perform fuzzy matching
// note that defaulting to fuzzy matching is set by the API
if matchPolicy == nil || *matchPolicy == v1.Exact {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
for _, namedRule := range namedRules {
for _, equivalent := range equivalents {
if equivalent == attr.GetResource() {
// we have already checked the original resource
continue
}
attrWithOverride.resource = equivalent
rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
m := rules.Matcher{
Rule: rule,
Attr: attrWithOverride,
}
if !m.Matches() {
continue
}
matchKind = o.GetEquivalentResourceMapper().KindFor(equivalent, attr.GetSubresource())
if matchKind.Empty() {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("unable to convert to %v: unknown kind", equivalent)
}
// an empty name list always matches
if len(namedRule.ResourceNames) == 0 {
return true, equivalent, matchKind, nil
}
// TODO: GetName() can return an empty string if the user is relying on
// the API server to generate the name... figure out what to do for this edge case
name := attr.GetName()
for _, matchedName := range namedRule.ResourceNames {
if name == matchedName {
return true, equivalent, matchKind, nil
}
}
}
}
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
type attrWithResourceOverride struct {
admission.Attributes
resource schema.GroupVersionResource
}
func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { return a.resource }

View File

@ -1,144 +0,0 @@
/*
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 mutating
import (
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
)
func NewMutatingAdmissionPolicyAccessor(obj *Policy) generic.PolicyAccessor {
return &mutatingAdmissionPolicyAccessor{
Policy: obj,
}
}
func NewMutatingAdmissionPolicyBindingAccessor(obj *PolicyBinding) generic.BindingAccessor {
return &mutatingAdmissionPolicyBindingAccessor{
PolicyBinding: obj,
}
}
type mutatingAdmissionPolicyAccessor struct {
*Policy
}
func (v *mutatingAdmissionPolicyAccessor) GetNamespace() string {
return v.Namespace
}
func (v *mutatingAdmissionPolicyAccessor) GetName() string {
return v.Name
}
func (v *mutatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind {
pk := v.Spec.ParamKind
if pk == nil {
return nil
}
return &v1.ParamKind{
APIVersion: pk.APIVersion,
Kind: pk.Kind,
}
}
func (v *mutatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources {
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchConstraints)
}
func (v *mutatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return toV1FailurePolicy(v.Spec.FailurePolicy)
}
func toV1FailurePolicy(failurePolicy *v1alpha1.FailurePolicyType) *v1.FailurePolicyType {
if failurePolicy == nil {
return nil
}
fp := v1.FailurePolicyType(*failurePolicy)
return &fp
}
type mutatingAdmissionPolicyBindingAccessor struct {
*PolicyBinding
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetNamespace() string {
return v.Namespace
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetName() string {
return v.Name
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
return types.NamespacedName{
Namespace: "",
Name: v.Spec.PolicyName,
}
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources {
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchResources)
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef {
if v.Spec.ParamRef == nil {
return nil
}
var nfa *v1.ParameterNotFoundActionType
if v.Spec.ParamRef.ParameterNotFoundAction != nil {
nfa = new(v1.ParameterNotFoundActionType)
*nfa = v1.ParameterNotFoundActionType(*v.Spec.ParamRef.ParameterNotFoundAction)
}
return &v1.ParamRef{
Name: v.Spec.ParamRef.Name,
Namespace: v.Spec.ParamRef.Namespace,
Selector: v.Spec.ParamRef.Selector,
ParameterNotFoundAction: nfa,
}
}
func convertV1alpha1ResourceRulesToV1(mc *v1alpha1.MatchResources) *v1.MatchResources {
if mc == nil {
return nil
}
var res v1.MatchResources
res.NamespaceSelector = mc.NamespaceSelector
res.ObjectSelector = mc.ObjectSelector
for _, ex := range mc.ExcludeResourceRules {
res.ExcludeResourceRules = append(res.ExcludeResourceRules, v1.NamedRuleWithOperations{
ResourceNames: ex.ResourceNames,
RuleWithOperations: ex.RuleWithOperations,
})
}
for _, ex := range mc.ResourceRules {
res.ResourceRules = append(res.ResourceRules, v1.NamedRuleWithOperations{
ResourceNames: ex.ResourceNames,
RuleWithOperations: ex.RuleWithOperations,
})
}
if mc.MatchPolicy != nil {
mp := v1.MatchPolicyType(*mc.MatchPolicy)
res.MatchPolicy = &mp
}
return &res
}

View File

@ -1,81 +0,0 @@
/*
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 mutating
import (
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
)
// compilePolicy compiles the policy into a PolicyEvaluator
// any error is stored and delayed until invocation.
//
// Each individual mutation is compiled into MutationEvaluationFunc and
// returned is a PolicyEvaluator in the same order as the mutations appeared in the policy.
func compilePolicy(policy *Policy) PolicyEvaluator {
opts := plugincel.OptionalVariableDeclarations{HasParams: policy.Spec.ParamKind != nil, StrictCost: true, HasAuthorizer: true}
compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
if err != nil {
return PolicyEvaluator{Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: fmt.Sprintf("failed to initialize CEL compiler: %v", err),
}}
}
// Compile and store variables
compiler.CompileAndStoreVariables(convertv1alpha1Variables(policy.Spec.Variables), opts, environment.StoredExpressions)
// Compile matchers
var matcher matchconditions.Matcher = nil
matchConditions := policy.Spec.MatchConditions
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]plugincel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(compiler.CompileCondition(matchExpressionAccessors, opts, environment.StoredExpressions), toV1FailurePolicy(policy.Spec.FailurePolicy), "policy", "validate", policy.Name)
}
// Compiler patchers
var patchers []patch.Patcher
patchOptions := opts
patchOptions.HasPatchTypes = true
for _, m := range policy.Spec.Mutations {
switch m.PatchType {
case v1alpha1.PatchTypeJSONPatch:
if m.JSONPatch != nil {
accessor := &patch.JSONPatchCondition{Expression: m.JSONPatch.Expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
patchers = append(patchers, patch.NewJSONPatcher(compileResult))
}
case v1alpha1.PatchTypeApplyConfiguration:
if m.ApplyConfiguration != nil {
accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult))
}
}
}
return PolicyEvaluator{Matcher: matcher, Mutators: patchers, CompositionEnv: compiler.CompositionEnv}
}

View File

@ -1,295 +0,0 @@
/*
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 mutating
import (
"context"
"errors"
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
"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/policy/mutating/patch"
webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] {
res := &dispatcher{
matcher: m,
authz: a,
typeConverterManager: tcm,
}
res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator](
NewMutatingAdmissionPolicyAccessor,
NewMutatingAdmissionPolicyBindingAccessor,
m,
res.dispatchInvocations,
)
return res
}
type dispatcher struct {
matcher *matching.Matcher
authz authorizer.Authorizer
typeConverterManager patch.TypeConverterManager
generic.Dispatcher[PolicyHook]
}
func (d *dispatcher) Start(ctx context.Context) error {
go d.typeConverterManager.Run(ctx)
return d.Dispatcher.Start(ctx)
}
func (d *dispatcher) dispatchInvocations(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
versionedAttributes webhookgeneric.VersionedAttributeAccessor,
invocations []generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator],
) ([]generic.PolicyError, *k8serrors.StatusError) {
var lastVersionedAttr *admission.VersionedAttributes
reinvokeCtx := a.GetReinvocationContext()
var policyReinvokeCtx *policyReinvokeContext
if v := reinvokeCtx.Value(PluginName); v != nil {
policyReinvokeCtx = v.(*policyReinvokeContext)
} else {
policyReinvokeCtx = &policyReinvokeContext{}
reinvokeCtx.SetValue(PluginName, policyReinvokeCtx)
}
if reinvokeCtx.IsReinvoke() && policyReinvokeCtx.IsOutputChangedSinceLastPolicyInvocation(a.GetObject()) {
// If the object has changed, we know the in-tree plugin re-invocations have mutated the object,
// and we need to reinvoke all eligible policies.
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
}
defer func() {
policyReinvokeCtx.SetLastPolicyInvocationOutput(a.GetObject())
}()
var policyErrors []generic.PolicyError
addConfigError := func(err error, invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator], reason metav1.StatusReason) {
policyErrors = append(policyErrors, generic.PolicyError{
Message: err,
Policy: NewMutatingAdmissionPolicyAccessor(invocation.Policy),
Binding: NewMutatingAdmissionPolicyBindingAccessor(invocation.Binding),
Reason: reason,
})
}
// There is at least one invocation to invoke. Make sure we have a namespace
// object if the incoming object is not cluster scoped to pass into the evaluator.
var namespace *v1.Namespace
var err error
namespaceName := a.GetNamespace()
// Special case, the namespace object has the namespace of itself (maybe a bug).
// unset it if the incoming object is a namespace
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
namespaceName = ""
}
// if it is cluster scoped, namespaceName will be empty
// Otherwise, get the Namespace resource.
if namespaceName != "" {
namespace, err = d.matcher.GetNamespace(namespaceName)
if err != nil {
return nil, k8serrors.NewNotFound(schema.GroupResource{Group: "", Resource: "namespaces"}, namespaceName)
}
}
authz := admissionauthorizer.NewCachingAuthorizer(d.authz)
// Should loop through invocations, handling possible error and invoking
// evaluator to apply patch, also should handle re-invocations
for _, invocation := range invocations {
if invocation.Evaluator.CompositionEnv != nil {
ctx = invocation.Evaluator.CompositionEnv.CreateContext(ctx)
}
if len(invocation.Evaluator.Mutators) != len(invocation.Policy.Spec.Mutations) {
// This would be a bug. The compiler should always return exactly as
// many evaluators as there are mutations
return nil, k8serrors.NewInternalError(fmt.Errorf("expected %v compiled evaluators for policy %v, got %v",
len(invocation.Policy.Spec.Mutations), invocation.Policy.Name, len(invocation.Evaluator.Mutators)))
}
versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind)
if err != nil {
// This should never happen, we pre-warm versoined attribute
// accessors before starting the dispatcher
return nil, k8serrors.NewInternalError(err)
}
if invocation.Evaluator.Matcher != nil {
matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz)
if matchResults.Error != nil {
addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid)
continue
}
// if preconditions are not met, then skip mutations
if !matchResults.Matches {
continue
}
}
invocationKey, invocationKeyErr := keyFor(invocation)
if invocationKeyErr != nil {
// This should never happen. It occurs if there is a programming
// error causing the Param not to be a valid object.
return nil, k8serrors.NewInternalError(invocationKeyErr)
}
if reinvokeCtx.IsReinvoke() && !policyReinvokeCtx.ShouldReinvoke(invocationKey) {
continue
}
objectBeforeMutations := versionedAttr.VersionedObject
// Mutations for a single invocation of a MutatingAdmissionPolicy are evaluated
// in order.
for mutationIndex := range invocation.Policy.Spec.Mutations {
lastVersionedAttr = versionedAttr
if versionedAttr.VersionedObject == nil { // Do not call patchers if there is no object to patch.
continue
}
patcher := invocation.Evaluator.Mutators[mutationIndex]
optionalVariables := cel.OptionalVariableBindings{VersionedParams: invocation.Param, Authorizer: authz}
err = d.dispatchOne(ctx, patcher, o, versionedAttr, namespace, invocation.Resource, optionalVariables)
if err != nil {
var statusError *k8serrors.StatusError
if errors.As(err, &statusError) {
return nil, statusError
}
addConfigError(err, invocation, metav1.StatusReasonInvalid)
continue
}
}
if !apiequality.Semantic.DeepEqual(objectBeforeMutations, versionedAttr.VersionedObject) {
// The mutation has changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation.
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
}
if invocation.Policy.Spec.ReinvocationPolicy == v1alpha1.IfNeededReinvocationPolicy {
policyReinvokeCtx.AddReinvocablePolicyToPreviouslyInvoked(invocationKey)
}
}
if lastVersionedAttr != nil && lastVersionedAttr.VersionedObject != nil && lastVersionedAttr.Dirty {
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
if err := o.GetObjectConvertor().Convert(lastVersionedAttr.VersionedObject, lastVersionedAttr.Attributes.GetObject(), nil); err != nil {
return nil, k8serrors.NewInternalError(fmt.Errorf("failed to convert object: %w", err))
}
}
return policyErrors, nil
}
func (d *dispatcher) dispatchOne(
ctx context.Context,
patcher patch.Patcher,
o admission.ObjectInterfaces,
versionedAttributes *admission.VersionedAttributes,
namespace *v1.Namespace,
resource schema.GroupVersionResource,
optionalVariables cel.OptionalVariableBindings,
) (err error) {
if patcher == nil {
// internal error. this should not happen
return k8serrors.NewInternalError(fmt.Errorf("policy evaluator is nil"))
}
// Find type converter for the invoked Group-Version.
typeConverter := d.typeConverterManager.GetTypeConverter(versionedAttributes.VersionedKind)
if typeConverter == nil {
// This can happen if the request is for a resource whose schema
// has not been registered with the type converter manager.
return k8serrors.NewServiceUnavailable(fmt.Sprintf("Resource kind %s not found. There can be a delay between when CustomResourceDefinitions are created and when they are available.", versionedAttributes.VersionedKind))
}
patchRequest := patch.Request{
MatchedResource: resource,
VersionedAttributes: versionedAttributes,
ObjectInterfaces: o,
OptionalVariables: optionalVariables,
Namespace: namespace,
TypeConverter: typeConverter,
}
newVersionedObject, err := patcher.Patch(ctx, patchRequest, celconfig.RuntimeCELCostBudget)
if err != nil {
return err
}
switch versionedAttributes.VersionedObject.(type) {
case *unstructured.Unstructured:
// No conversion needed before defaulting for the patch object if the admitted object is unstructured.
default:
// Before defaulting, if the admitted object is a typed object, convert unstructured patch result back to a typed object.
newVersionedObject, err = o.GetObjectConvertor().ConvertToVersion(newVersionedObject, versionedAttributes.GetKind().GroupVersion())
if err != nil {
return err
}
}
o.GetObjectDefaulter().Default(newVersionedObject)
versionedAttributes.Dirty = true
versionedAttributes.VersionedObject = newVersionedObject
return nil
}
func keyFor(invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator]) (key, error) {
var paramUID types.NamespacedName
if invocation.Param != nil {
paramAccessor, err := meta.Accessor(invocation.Param)
if err != nil {
// This should never happen, as the param should have been validated
// before being passed to the plugin.
return key{}, err
}
paramUID = types.NamespacedName{
Name: paramAccessor.GetName(),
Namespace: paramAccessor.GetNamespace(),
}
}
return key{
PolicyUID: types.NamespacedName{
Name: invocation.Policy.GetName(),
Namespace: invocation.Policy.GetNamespace(),
},
BindingUID: types.NamespacedName{
Name: invocation.Binding.GetName(),
Namespace: invocation.Binding.GetNamespace(),
},
ParamUID: paramUID,
}, nil
}

View File

@ -1,45 +0,0 @@
/*
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 patch
import (
"context"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
// Patcher provides a patch function to perform a mutation to an object in the admission chain.
type Patcher interface {
// Patch returns a copy of the object in the request, modified to change specified by the patch.
// The original object in the request MUST NOT be modified in-place.
Patch(ctx context.Context, request Request, runtimeCELCostBudget int64) (runtime.Object, error)
}
// Request defines the arguments required by a patcher.
type Request struct {
MatchedResource schema.GroupVersionResource
VersionedAttributes *admission.VersionedAttributes
ObjectInterfaces admission.ObjectInterfaces
OptionalVariables cel.OptionalVariableBindings
Namespace *v1.Namespace
TypeConverter managedfields.TypeConverter
}

View File

@ -1,192 +0,0 @@
/*
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 patch
import (
"context"
gojson "encoding/json"
"errors"
"fmt"
celgo "github.com/google/cel-go/cel"
"reflect"
"strconv"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/traits"
"google.golang.org/protobuf/types/known/structpb"
jsonpatch "gopkg.in/evanphx/json-patch.v4"
admissionv1 "k8s.io/api/admission/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/cel/mutation"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
pointer "k8s.io/utils/ptr"
)
// JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression
// that returns a JSON patch value.
type JSONPatchCondition struct {
Expression string
}
var _ plugincel.ExpressionAccessor = &JSONPatchCondition{}
func (v *JSONPatchCondition) GetExpression() string {
return v.Expression
}
func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.ListType(jsonPatchType)}
}
var jsonPatchType = types.NewObjectType("JSONPatch")
// NewJSONPatcher creates a patcher that performs a JSON Patch mutation.
func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher {
return &jsonPatcher{patchEvaluator}
}
type jsonPatcher struct {
PatchEvaluator plugincel.MutatingEvaluator
}
func (e *jsonPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
admissionRequest := plugincel.CreateAdmissionRequest(
r.VersionedAttributes.Attributes,
metav1.GroupVersionResource(r.MatchedResource),
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
compileErrors := e.PatchEvaluator.CompilationErrors()
if len(compileErrors) > 0 {
return nil, errors.Join(compileErrors...)
}
patchObj, _, err := e.evaluatePatchExpression(ctx, e.PatchEvaluator, runtimeCELCostBudget, r, admissionRequest)
if err != nil {
return nil, err
}
o := r.ObjectInterfaces
jsonSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), json.SerializerOptions{Pretty: false, Strict: true})
objJS, err := runtime.Encode(jsonSerializer, r.VersionedAttributes.VersionedObject)
if err != nil {
return nil, fmt.Errorf("failed to create JSON patch: %w", err)
}
patchedJS, err := patchObj.Apply(objJS)
if err != nil {
if errors.Is(err, jsonpatch.ErrTestFailed) {
// If a json patch fails a test operation, the patch must not be applied
return r.VersionedAttributes.VersionedObject, nil
}
return nil, fmt.Errorf("JSON Patch: %w", err)
}
var newVersionedObject runtime.Object
if _, ok := r.VersionedAttributes.VersionedObject.(*unstructured.Unstructured); ok {
newVersionedObject = &unstructured.Unstructured{}
} else {
newVersionedObject, err = o.GetObjectCreater().New(r.VersionedAttributes.VersionedKind)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
}
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
return nil, apierrors.NewInternalError(err)
}
return newVersionedObject, nil
}
func (e *jsonPatcher) evaluatePatchExpression(ctx context.Context, patchEvaluator plugincel.MutatingEvaluator, remainingBudget int64, r Request, admissionRequest *admissionv1.AdmissionRequest) (jsonpatch.Patch, int64, error) {
var err error
var eval plugincel.EvaluationResult
eval, remainingBudget, err = patchEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, remainingBudget)
if err != nil {
return nil, -1, err
}
if eval.Error != nil {
return nil, -1, eval.Error
}
refVal := eval.EvalResult
// the return type can be any valid CEL value.
// Scalars, maps and lists are used to set the value when the path points to a field of that type.
// ObjectVal is used when the path points to a struct. A map like "{"field1": 1, "fieldX": bool}" is not
// possible in Kubernetes CEL because maps and lists may not have mixed types.
iter, ok := refVal.(traits.Lister)
if !ok {
// Should never happen since compiler checks return type.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array")
}
result := jsonpatch.Patch{}
for it := iter.Iterator(); it.HasNext() == types.True; {
v := it.Next()
patchObj, err := v.ConvertToNative(reflect.TypeOf(&mutation.JSONPatchVal{}))
if err != nil {
// Should never happen since return type is checked by compiler.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch: %w", err)
}
op, ok := patchObj.(*mutation.JSONPatchVal)
if !ok {
// Should never happen since return type is checked by compiler.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch, got element of %T", patchObj)
}
// Construct a JSON Patch from the evaluated CEL expression
resultOp := jsonpatch.Operation{}
resultOp["op"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Op)))
resultOp["path"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Path)))
if len(op.From) > 0 {
resultOp["from"] = pointer.To(gojson.RawMessage(strconv.Quote(op.From)))
}
if op.Val != nil {
if objVal, ok := op.Val.(*dynamic.ObjectVal); ok {
// TODO: Object initializers are insufficiently type checked.
// In the interim, we use this sanity check to detect type mismatches
// between field names and Object initializers. For example,
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
// Before beta, attaching full type information both to Object initializers and
// the "object" and "oldObject" variables is needed. This will allow CEL to
// perform comprehensive runtime type checking.
err := objVal.CheckTypeNamesMatchFieldPathNames()
if err != nil {
return nil, -1, fmt.Errorf("type mismatch: %w", err)
}
}
// CEL data literals representing arbitrary JSON values can be serialized to JSON for use in
// JSON Patch if first converted to pb.Value.
v, err := op.Val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
if err != nil {
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
}
b, err := gojson.Marshal(v)
if err != nil {
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
}
resultOp["value"] = pointer.To[gojson.RawMessage](b)
}
result = append(result, resultOp)
}
return result, remainingBudget, nil
}

View File

@ -1,217 +0,0 @@
/*
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 patch
import (
"context"
"errors"
"fmt"
celgo "github.com/google/cel-go/cel"
celtypes "github.com/google/cel-go/common/types"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v4/value"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/managedfields"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
)
// ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression
// that returns an apply configuration
type ApplyConfigurationCondition struct {
Expression string
}
var _ plugincel.ExpressionAccessor = &ApplyConfigurationCondition{}
func (v *ApplyConfigurationCondition) GetExpression() string {
return v.Expression
}
func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{applyConfigObjectType}
}
var applyConfigObjectType = celtypes.NewObjectType("Object")
// NewApplyConfigurationPatcher creates a patcher that performs an applyConfiguration mutation.
func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher {
return &applyConfigPatcher{expressionEvaluator: expressionEvaluator}
}
type applyConfigPatcher struct {
expressionEvaluator plugincel.MutatingEvaluator
}
func (e *applyConfigPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
admissionRequest := plugincel.CreateAdmissionRequest(
r.VersionedAttributes.Attributes,
metav1.GroupVersionResource(r.MatchedResource),
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
compileErrors := e.expressionEvaluator.CompilationErrors()
if len(compileErrors) > 0 {
return nil, errors.Join(compileErrors...)
}
eval, _, err := e.expressionEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, runtimeCELCostBudget)
if err != nil {
return nil, err
}
if eval.Error != nil {
return nil, eval.Error
}
v := eval.EvalResult
// The compiler ensures that the return type is an ObjectVal with type name of "Object".
objVal, ok := v.(*dynamic.ObjectVal)
if !ok {
// Should not happen since the compiler type checks the return type.
return nil, fmt.Errorf("unsupported return type from ApplyConfiguration expression: %v", v.Type())
}
// TODO: Object initializers are insufficiently type checked.
// In the interim, we use this sanity check to detect type mismatches
// between field names and Object initializers. For example,
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
// Before beta, attaching full type information both to Object initializers and
// the "object" and "oldObject" variables is needed. This will allow CEL to
// perform comprehensive runtime type checking.
err = objVal.CheckTypeNamesMatchFieldPathNames()
if err != nil {
return nil, fmt.Errorf("type mismatch: %w", err)
}
value, ok := objVal.Value().(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid return type: %T", v)
}
patchObject := unstructured.Unstructured{Object: value}
patchObject.SetGroupVersionKind(r.VersionedAttributes.VersionedObject.GetObjectKind().GroupVersionKind())
patched, err := ApplyStructuredMergeDiff(r.TypeConverter, r.VersionedAttributes.VersionedObject, &patchObject)
if err != nil {
return nil, fmt.Errorf("error applying patch: %w", err)
}
return patched, nil
}
// ApplyStructuredMergeDiff applies a structured merge diff to an object and returns a copy of the object
// with the patch applied.
func ApplyStructuredMergeDiff(
typeConverter managedfields.TypeConverter,
originalObject runtime.Object,
patch *unstructured.Unstructured,
) (runtime.Object, error) {
if patch.GroupVersionKind() != originalObject.GetObjectKind().GroupVersionKind() {
return nil, fmt.Errorf("patch (%v) and original object (%v) are not of the same gvk", patch.GroupVersionKind().String(), originalObject.GetObjectKind().GroupVersionKind().String())
} else if typeConverter == nil {
return nil, fmt.Errorf("type converter must not be nil")
}
patchObjTyped, err := typeConverter.ObjectToTyped(patch)
if err != nil {
return nil, fmt.Errorf("failed to convert patch object to typed object: %w", err)
}
err = validatePatch(patchObjTyped)
if err != nil {
return nil, fmt.Errorf("invalid ApplyConfiguration: %w", err)
}
liveObjTyped, err := typeConverter.ObjectToTyped(originalObject)
if err != nil {
return nil, fmt.Errorf("failed to convert original object to typed object: %w", err)
}
newObjTyped, err := liveObjTyped.Merge(patchObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to merge patch: %w", err)
}
// Our mutating admission policy sets the fields but does not track ownership.
// Newly introduced fields in the patch won't be tracked by a field manager
// (so if the original object is updated again but the mutating policy is
// not active, the fields will be dropped).
//
// This necessarily means that changes to an object by a mutating policy
// are only preserved if the policy was active at the time of the change.
// (If the policy is not active, the changes may be dropped.)
newObj, err := typeConverter.TypedToObject(newObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to convert typed object to object: %w", err)
}
return newObj, nil
}
// validatePatch searches an apply configuration for any arrays, maps or structs elements that are atomic and returns
// an error if any are found.
// This prevents accidental removal of fields that can occur when the user intends to modify some
// fields in an atomic type, not realizing that all fields not explicitly set in the new value
// of the atomic will be removed.
func validatePatch(v *typed.TypedValue) error {
atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue())
if len(atomics) > 0 {
return fmt.Errorf("may not mutate atomic arrays, maps or structs: %v", strings.Join(atomics, ", "))
}
return nil
}
// findAtomics returns field paths for any atomic arrays, maps or structs found when traversing the given value.
func findAtomics(path []fieldpath.PathElement, s *schema.Schema, tr schema.TypeRef, v value.Value) (atomics []string) {
if a, ok := s.Resolve(tr); ok { // Validation pass happens before this and checks that all schemas can be resolved
if v.IsMap() && a.Map != nil {
if a.Map.ElementRelationship == schema.Atomic {
atomics = append(atomics, pathString(path))
}
v.AsMap().Iterate(func(key string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &key}
if sf, ok := a.Map.FindField(key); ok {
tr = sf.Type
atomics = append(atomics, findAtomics(append(path, pe), s, tr, val)...)
}
return true
})
}
if v.IsList() && a.List != nil {
if a.List.ElementRelationship == schema.Atomic {
atomics = append(atomics, pathString(path))
}
list := v.AsList()
for i := 0; i < list.Length(); i++ {
pe := fieldpath.PathElement{Index: &i}
atomics = append(atomics, findAtomics(append(path, pe), s, a.List.ElementType, list.At(i))...)
}
}
}
return atomics
}
func pathString(path []fieldpath.PathElement) string {
sb := strings.Builder{}
for _, p := range path {
sb.WriteString(p.String())
}
return sb.String()
}

View File

@ -1,187 +0,0 @@
/*
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 patch
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/openapi"
"k8s.io/kube-openapi/pkg/spec3"
)
type TypeConverterManager interface {
// GetTypeConverter returns a type converter for the given GVK
GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter
Run(ctx context.Context)
}
func NewTypeConverterManager(
staticTypeConverter managedfields.TypeConverter,
openapiClient openapi.Client,
) TypeConverterManager {
return &typeConverterManager{
staticTypeConverter: staticTypeConverter,
openapiClient: openapiClient,
typeConverterMap: make(map[schema.GroupVersion]typeConverterCacheEntry),
lastFetchedPaths: make(map[schema.GroupVersion]openapi.GroupVersion),
}
}
type typeConverterCacheEntry struct {
typeConverter managedfields.TypeConverter
entry openapi.GroupVersion
}
// typeConverterManager helps us make sure we have an up to date schema and
// type converter for our openapi models. It should be connfigured to use a
// static type converter for natively typed schemas, and fetches the schema
// for CRDs/other over the network on demand (trying to reduce network calls where necessary)
type typeConverterManager struct {
// schemaCache is used to cache the schema for a given GVK
staticTypeConverter managedfields.TypeConverter
// discoveryClient is used to fetch the schema for a given GVK
openapiClient openapi.Client
lock sync.RWMutex
typeConverterMap map[schema.GroupVersion]typeConverterCacheEntry
lastFetchedPaths map[schema.GroupVersion]openapi.GroupVersion
}
func (t *typeConverterManager) Run(ctx context.Context) {
// Loop every 5s refershing the OpenAPI schema list to know which
// schemas have been invalidated. This should use e-tags under the hood
_ = wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(_ context.Context) (done bool, err error) {
paths, err := t.openapiClient.Paths()
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to fetch openapi paths: %w", err))
return false, nil
}
// The /openapi/v3 endpoint contains a list of paths whose ServerRelativeURL
// value changes every time the schema is updated. So we poll /openapi/v3
// to get the "version number" for each schema, and invalidate our cache
// if the version number has changed since we pulled it.
parsedPaths := make(map[schema.GroupVersion]openapi.GroupVersion, len(paths))
for path, entry := range paths {
if !strings.HasPrefix(path, "apis/") && !strings.HasPrefix(path, "api/") {
continue
}
path = strings.TrimPrefix(path, "apis/")
path = strings.TrimPrefix(path, "api/")
gv, err := schema.ParseGroupVersion(path)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to parse group version %q: %w", path, err))
return false, nil
}
parsedPaths[gv] = entry
}
t.lock.Lock()
defer t.lock.Unlock()
t.lastFetchedPaths = parsedPaths
return false, nil
})
}
func (t *typeConverterManager) GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter {
// Check to see if the static type converter handles this GVK
if t.staticTypeConverter != nil {
//!TODO: Add ability to check existence to type converter
// working around for now but seeing if getting a typed version of an
// empty object returns error
stub := &unstructured.Unstructured{}
stub.SetGroupVersionKind(gvk)
if _, err := t.staticTypeConverter.ObjectToTyped(stub); err == nil {
return t.staticTypeConverter
}
}
gv := gvk.GroupVersion()
existing, entry, err := func() (managedfields.TypeConverter, openapi.GroupVersion, error) {
t.lock.RLock()
defer t.lock.RUnlock()
// If schema is not supported by static type converter, ask discovery
// for the schema
entry, ok := t.lastFetchedPaths[gv]
if !ok {
// If we can't get the schema, we can't do anything
return nil, nil, fmt.Errorf("no schema for %v", gvk)
}
// If the entry schema has not changed, used the same type converter
if existing, ok := t.typeConverterMap[gv]; ok && existing.entry.ServerRelativeURL() == entry.ServerRelativeURL() {
// If we have a type converter for this GVK, return it
return existing.typeConverter, existing.entry, nil
}
return nil, entry, nil
}()
if err != nil {
utilruntime.HandleError(err)
return nil
} else if existing != nil {
return existing
}
schBytes, err := entry.Schema(runtime.ContentTypeJSON)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to get schema for %v: %w", gvk, err))
return nil
}
var sch spec3.OpenAPI
if err := json.Unmarshal(schBytes, &sch); err != nil {
utilruntime.HandleError(fmt.Errorf("failed to unmarshal schema for %v: %w", gvk, err))
return nil
}
// The schema has changed, or there is no entry for it, generate
// a new type converter for this GV
tc, err := managedfields.NewTypeConverter(sch.Components.Schemas, false)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to create type converter for %v: %w", gvk, err))
return nil
}
t.lock.Lock()
defer t.lock.Unlock()
t.typeConverterMap[gv] = typeConverterCacheEntry{
typeConverter: tc,
entry: entry,
}
return tc
}

View File

@ -1,151 +0,0 @@
/*
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 mutating
import (
"context"
celgo "github.com/google/cel-go/cel"
"io"
"k8s.io/api/admissionregistration/v1alpha1"
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/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"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/policy/mutating/patch"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/authorization/authorizer"
"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 = "MutatingAdmissionPolicy"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
return NewPlugin(configFile), nil
})
}
type Policy = v1alpha1.MutatingAdmissionPolicy
type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding
type PolicyMutation = v1alpha1.Mutation
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
type Mutator struct {
}
type MutationEvaluationFunc func(
ctx context.Context,
matchedResource schema.GroupVersionResource,
versionedAttr *admission.VersionedAttributes,
o admission.ObjectInterfaces,
versionedParams runtime.Object,
namespace *corev1.Namespace,
typeConverter managedfields.TypeConverter,
runtimeCELCostBudget int64,
authorizer authorizer.Authorizer,
) (runtime.Object, error)
type PolicyEvaluator struct {
Matcher matchconditions.Matcher
Mutators []patch.Patcher
CompositionEnv *cel.CompositionEnv
Error error
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*generic.Plugin[PolicyHook]
}
var _ admission.Interface = &Plugin{}
var _ admission.MutationInterface = &Plugin{}
// NewPlugin returns a generic admission webhook plugin.
func NewPlugin(_ io.Reader) *Plugin {
// There is no request body to mutate for DELETE, so this plugin never handles that operation.
handler := admission.NewHandler(admission.Create, admission.Update, admission.Connect)
res := &Plugin{}
res.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().V1alpha1().MutatingAdmissionPolicies().Informer(),
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicyBindings().Informer(),
NewMutatingAdmissionPolicyAccessor,
NewMutatingAdmissionPolicyBindingAccessor,
compilePolicy,
//!TODO: Create a way to share param informers between
// mutating/validating plugins
f,
dynamicClient,
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, m, patch.NewTypeConverterManager(nil, client.Discovery().OpenAPIV3()))
},
)
return res
}
// Admit makes an admission decision based on the request attributes.
func (a *Plugin) Admit(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.MutatingAdmissionPolicy))
}
// Variable is a named expression for composition.
type Variable struct {
Name string
Expression string
}
func (v *Variable) GetExpression() string {
return v.Expression
}
func (v *Variable) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.AnyType, celgo.DynType}
}
func (v *Variable) GetName() string {
return v.Name
}
func convertv1alpha1Variables(variables []v1alpha1.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
}

View File

@ -1,76 +0,0 @@
/*
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 mutating
import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
)
type key struct {
PolicyUID types.NamespacedName
BindingUID types.NamespacedName
ParamUID types.NamespacedName
MutationIndex int
}
type policyReinvokeContext struct {
// lastPolicyOutput holds the result of the last Policy admission plugin call
lastPolicyOutput runtime.Object
// previouslyInvokedReinvocablePolicys holds the set of policies that have been invoked and
// should be reinvoked if a later mutation occurs
previouslyInvokedReinvocablePolicies sets.Set[key]
// reinvokePolicies holds the set of Policies that should be reinvoked
reinvokePolicies sets.Set[key]
}
func (rc *policyReinvokeContext) ShouldReinvoke(policy key) bool {
return rc.reinvokePolicies.Has(policy)
}
func (rc *policyReinvokeContext) IsOutputChangedSinceLastPolicyInvocation(object runtime.Object) bool {
return !apiequality.Semantic.DeepEqual(rc.lastPolicyOutput, object)
}
func (rc *policyReinvokeContext) SetLastPolicyInvocationOutput(object runtime.Object) {
if object == nil {
rc.lastPolicyOutput = nil
return
}
rc.lastPolicyOutput = object.DeepCopyObject()
}
func (rc *policyReinvokeContext) AddReinvocablePolicyToPreviouslyInvoked(policy key) {
if rc.previouslyInvokedReinvocablePolicies == nil {
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
}
rc.previouslyInvokedReinvocablePolicies.Insert(policy)
}
func (rc *policyReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() {
if len(rc.previouslyInvokedReinvocablePolicies) > 0 {
if rc.reinvokePolicies == nil {
rc.reinvokePolicies = sets.New[key]()
}
for s := range rc.previouslyInvokedReinvocablePolicies {
rc.reinvokePolicies.Insert(s)
}
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
}
}

View File

@ -1,86 +0,0 @@
/*
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/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
)
func NewValidatingAdmissionPolicyAccessor(obj *v1.ValidatingAdmissionPolicy) generic.PolicyAccessor {
return &validatingAdmissionPolicyAccessor{
ValidatingAdmissionPolicy: obj,
}
}
func NewValidatingAdmissionPolicyBindingAccessor(obj *v1.ValidatingAdmissionPolicyBinding) generic.BindingAccessor {
return &validatingAdmissionPolicyBindingAccessor{
ValidatingAdmissionPolicyBinding: obj,
}
}
type validatingAdmissionPolicyAccessor struct {
*v1.ValidatingAdmissionPolicy
}
func (v *validatingAdmissionPolicyAccessor) GetNamespace() string {
return v.Namespace
}
func (v *validatingAdmissionPolicyAccessor) GetName() string {
return v.Name
}
func (v *validatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind {
return v.Spec.ParamKind
}
func (v *validatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources {
return v.Spec.MatchConstraints
}
func (v *validatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return v.Spec.FailurePolicy
}
type validatingAdmissionPolicyBindingAccessor struct {
*v1.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,
}
}
func (v *validatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources {
return v.Spec.MatchResources
}
func (v *validatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef {
return v.Spec.ParamRef
}

View File

@ -1,420 +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"
"strings"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utiljson "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apiserver/pkg/admission"
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
"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/klog/v2"
)
type dispatcher struct {
matcher generic.PolicyMatcher
authz authorizer.Authorizer
}
var _ generic.Dispatcher[PolicyHook] = &dispatcher{}
func NewDispatcher(
authorizer authorizer.Authorizer,
matcher generic.PolicyMatcher,
) generic.Dispatcher[PolicyHook] {
return &dispatcher{
matcher: matcher,
authz: authorizer,
}
}
// contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
// that determined the decision
type policyDecisionWithMetadata struct {
PolicyDecision
Definition *admissionregistrationv1.ValidatingAdmissionPolicy
Binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding
}
func (c *dispatcher) Start(ctx context.Context) error {
return nil
}
// Dispatch implements generic.Dispatcher.
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
var deniedDecisions []policyDecisionWithMetadata
addConfigError := func(err error, definition *admissionregistrationv1.ValidatingAdmissionPolicy, binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding) {
// we always default the FailurePolicy if it is unset and validate it in API level
var policy admissionregistrationv1.FailurePolicyType
if definition.Spec.FailurePolicy == nil {
policy = admissionregistrationv1.Fail
} else {
policy = *definition.Spec.FailurePolicy
}
// apply FailurePolicy specified in ValidatingAdmissionPolicy, the default would be Fail
switch policy {
case admissionregistrationv1.Ignore:
// TODO: add metrics for ignored error here
return
case admissionregistrationv1.Fail:
var message string
if binding == nil {
message = fmt.Errorf("failed to configure policy: %w", err).Error()
} else {
message = fmt.Errorf("failed to configure binding: %w", err).Error()
}
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Message: message,
},
Definition: definition,
Binding: binding,
})
default:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Message: fmt.Errorf("unrecognized failure policy: '%v'", policy).Error(),
},
Definition: definition,
Binding: binding,
})
}
}
authz := admissionauthorizer.NewCachingAuthorizer(c.authz)
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 := hook.Policy
matches, matchResource, matchKind, err := c.matcher.DefinitionMatches(a, o, NewValidatingAdmissionPolicyAccessor(definition))
if err != nil {
// Configuration error.
addConfigError(err, definition, nil)
continue
}
if !matches {
// Policy definition does not match request
continue
} else if hook.ConfigurationError != nil {
// Configuration error.
addConfigError(hook.ConfigurationError, definition, nil)
continue
}
auditAnnotationCollector := newAuditAnnotationCollector()
for _, binding := range hook.Bindings {
// If the key is inside dependentBindings, there is guaranteed to
// be a bindingInfo for it
matches, err := c.matcher.BindingMatches(a, o, NewValidatingAdmissionPolicyBindingAccessor(binding))
if err != nil {
// Configuration error.
addConfigError(err, definition, binding)
continue
}
if !matches {
continue
}
params, err := generic.CollectParams(
hook.Policy.Spec.ParamKind,
hook.ParamInformer,
hook.ParamScope,
binding.Spec.ParamRef,
a.GetNamespace(),
)
if err != nil {
addConfigError(err, definition, binding)
continue
} else if versionedAttr == nil && len(params) > 0 {
// As optimization versionedAttr creation is deferred until
// first use. Since > 0 params, we will validate
va, err := admission.NewVersionedAttributes(a, matchKind, o)
if err != nil {
wrappedErr := fmt.Errorf("failed to convert object version: %w", err)
addConfigError(wrappedErr, definition, binding)
continue
}
versionedAttr = va
}
var validationResults []ValidateResult
var namespace *v1.Namespace
namespaceName := a.GetNamespace()
// Special case, the namespace object has the namespace of itself (maybe a bug).
// unset it if the incoming object is a namespace
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
namespaceName = ""
}
// if it is cluster scoped, namespaceName will be empty
// Otherwise, get the Namespace resource.
if namespaceName != "" {
namespace, err = c.matcher.GetNamespace(namespaceName)
if err != nil {
return err
}
}
for _, param := range params {
var p runtime.Object = param
if p != nil && p.GetObjectKind().GroupVersionKind().Empty() {
// Make sure param has TypeMeta populated
// This is a simple hack to make sure typeMeta is
// available to CEL without making copies of objects, etc.
p = &wrappedParam{
TypeMeta: metav1.TypeMeta{
APIVersion: definition.Spec.ParamKind.APIVersion,
Kind: definition.Spec.ParamKind.Kind,
},
nested: param,
}
}
validationResults = append(validationResults,
hook.Evaluator.Validate(
ctx,
matchResource,
versionedAttr,
p,
namespace,
celconfig.RuntimeCELCostBudget,
authz,
),
)
}
for _, validationResult := range validationResults {
for i, decision := range validationResult.Decisions {
switch decision.Action {
case ActionAdmit:
if decision.Evaluation == EvalError {
celmetrics.Metrics.ObserveAdmission(ctx, decision.Elapsed, definition.Name, binding.Name, ErrorType(&decision))
}
case ActionDeny:
for _, action := range binding.Spec.ValidationActions {
switch action {
case admissionregistrationv1.Deny:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: decision,
})
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, ErrorType(&decision))
case admissionregistrationv1.Audit:
publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, ErrorType(&decision))
case admissionregistrationv1.Warn:
warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
celmetrics.Metrics.ObserveWarn(ctx, decision.Elapsed, definition.Name, binding.Name, ErrorType(&decision))
}
}
default:
return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
decision.Action, binding.Name, definition.Name)
}
}
for _, auditAnnotation := range validationResult.AuditAnnotations {
switch auditAnnotation.Action {
case AuditAnnotationActionPublish:
value := auditAnnotation.Value
if len(auditAnnotation.Value) > maxAuditAnnotationValueLength {
value = value[:maxAuditAnnotationValueLength]
}
auditAnnotationCollector.add(auditAnnotation.Key, value)
case AuditAnnotationActionError:
// When failurePolicy=fail, audit annotation errors result in deny
d := policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Evaluation: EvalError,
Message: auditAnnotation.Error,
Elapsed: auditAnnotation.Elapsed,
},
}
deniedDecisions = append(deniedDecisions, d)
celmetrics.Metrics.ObserveRejection(ctx, auditAnnotation.Elapsed, definition.Name, binding.Name, ErrorType(&d.PolicyDecision))
case AuditAnnotationActionExclude: // skip it
default:
return fmt.Errorf("unsupported AuditAnnotation Action: %s", auditAnnotation.Action)
}
}
}
}
auditAnnotationCollector.publish(definition.Name, a)
}
if len(deniedDecisions) > 0 {
// TODO: refactor admission.NewForbidden so the name extraction is reusable but the code/reason is customizable
var message string
deniedDecision := deniedDecisions[0]
if deniedDecision.Binding != nil {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' with binding '%s' denied request: %s", deniedDecision.Definition.Name, deniedDecision.Binding.Name, deniedDecision.Message)
} else {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' denied request: %s", deniedDecision.Definition.Name, deniedDecision.Message)
}
err := admission.NewForbidden(a, errors.New(message)).(*k8serrors.StatusError)
reason := deniedDecision.Reason
if len(reason) == 0 {
reason = metav1.StatusReasonInvalid
}
err.ErrStatus.Reason = reason
err.ErrStatus.Code = reasonToCode(reason)
err.ErrStatus.Details.Causes = append(err.ErrStatus.Details.Causes, metav1.StatusCause{Message: message})
return err
}
return nil
}
func publishValidationFailureAnnotation(binding *admissionregistrationv1.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{{
ExpressionIndex: expressionIndex,
Message: decision.Message,
ValidationActions: binding.Spec.ValidationActions,
Binding: binding.Name,
Policy: binding.Spec.PolicyName,
}})
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)
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)
}
}
const maxAuditAnnotationValueLength = 10 * 1024
// validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
// annotation value.
type ValidationFailureValue struct {
Message string `json:"message"`
Policy string `json:"policy"`
Binding string `json:"binding"`
ExpressionIndex int `json:"expressionIndex"`
ValidationActions []admissionregistrationv1.ValidationAction `json:"validationActions"`
}
type auditAnnotationCollector struct {
annotations map[string][]string
}
func newAuditAnnotationCollector() auditAnnotationCollector {
return auditAnnotationCollector{annotations: map[string][]string{}}
}
func (a auditAnnotationCollector) add(key, value string) {
// If multiple bindings produces the exact same key and value for an audit annotation,
// ignore the duplicates.
for _, v := range a.annotations[key] {
if v == value {
return
}
}
a.annotations[key] = append(a.annotations[key], value)
}
func (a auditAnnotationCollector) publish(policyName string, attributes admission.Attributes) {
for key, bindingAnnotations := range a.annotations {
var value string
if len(bindingAnnotations) == 1 {
value = bindingAnnotations[0]
} else {
// Multiple distinct values can exist when binding params are used in the valueExpression of an auditAnnotation.
// When this happens, the values are concatenated into a comma-separated list.
value = strings.Join(bindingAnnotations, ", ")
}
if err := attributes.AddAnnotation(policyName+"/"+key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s: %v", key, value, policyName, err)
}
}
}
// A workaround to fact that native types do not have TypeMeta populated, which
// is needed for CEL expressions to be able to access the value.
type wrappedParam struct {
metav1.TypeMeta
nested runtime.Object
}
func (w *wrappedParam) MarshalJSON() ([]byte, error) {
return nil, errors.New("MarshalJSON unimplemented for wrappedParam")
}
func (w *wrappedParam) UnmarshalJSON(data []byte) error {
return errors.New("UnmarshalJSON unimplemented for wrappedParam")
}
func (w *wrappedParam) ToUnstructured() interface{} {
res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(w.nested)
if err != nil {
return nil
}
metaRes, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&w.TypeMeta)
if err != nil {
return nil
}
for k, v := range metaRes {
res[k] = v
}
return res
}
func (w *wrappedParam) DeepCopyObject() runtime.Object {
return &wrappedParam{
TypeMeta: w.TypeMeta,
nested: w.nested.DeepCopyObject(),
}
}
func (w *wrappedParam) GetObjectKind() schema.ObjectKind {
return w
}

View File

@ -1,38 +0,0 @@
/*
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 (
"strings"
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
)
// ErrorType decodes the error to determine the error type
// that the metrics understand.
func ErrorType(decision *PolicyDecision) celmetrics.ValidationErrorType {
if decision.Evaluation == EvalAdmit {
return celmetrics.ValidationNoError
}
if strings.HasPrefix(decision.Message, "compilation") {
return celmetrics.ValidationCompileError
}
if strings.HasPrefix(decision.Message, "validation failed due to running out of cost budget") {
return celmetrics.ValidatingOutOfBudget
}
return celmetrics.ValidatingInvalidError
}

View File

@ -1,31 +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"
"k8s.io/apiserver/pkg/admission"
)
type CELPolicyEvaluator interface {
admission.InitializationValidator
Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error
HasSynced() bool
Run(stopCh <-chan struct{})
}

View File

@ -1,95 +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"
celgo "github.com/google/cel-go/cel"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
var _ cel.ExpressionAccessor = &ValidationCondition{}
// ValidationCondition contains the inputs needed to compile, evaluate and validate a cel expression
type ValidationCondition struct {
Expression string
Message string
Reason *metav1.StatusReason
}
func (v *ValidationCondition) GetExpression() string {
return v.Expression
}
func (v *ValidationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
// AuditAnnotationCondition contains the inputs needed to compile, evaluate and publish a cel audit annotation
type AuditAnnotationCondition struct {
Key string
ValueExpression string
}
func (v *AuditAnnotationCondition) GetExpression() string {
return v.ValueExpression
}
func (v *AuditAnnotationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.StringType, celgo.NullType}
}
// Variable is a named expression for composition.
type Variable struct {
Name string
Expression string
}
func (v *Variable) GetExpression() string {
return v.Expression
}
func (v *Variable) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.AnyType, celgo.DynType}
}
func (v *Variable) GetName() string {
return v.Name
}
// ValidateResult defines the result of a Validator.Validate operation.
type ValidateResult struct {
// Decisions specifies the outcome of the validation as well as the details about the decision.
Decisions []PolicyDecision
// AuditAnnotations specifies the audit annotations that should be recorded for the validation.
AuditAnnotations []PolicyAuditAnnotation
}
// Validator is contains logic for converting ValidationEvaluation to PolicyDecisions
type Validator interface {
// Validate is used to take cel evaluations and convert into decisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
Validate(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
}

View File

@ -1,36 +0,0 @@
/*
Copyright 2023 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 (
celgo "github.com/google/cel-go/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
var _ cel.ExpressionAccessor = (*MessageExpressionCondition)(nil)
type MessageExpressionCondition struct {
MessageExpression string
}
func (m *MessageExpressionCondition) GetExpression() string {
return m.MessageExpression
}
func (m *MessageExpressionCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.StringType}
}

View File

@ -1,38 +0,0 @@
/*
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 metrics
import (
"errors"
apiservercel "k8s.io/apiserver/pkg/cel"
)
// ErrorType decodes the error to determine the error type
// that the metrics understand.
func ErrorType(err error) ValidationErrorType {
if err == nil {
return ValidationNoError
}
if errors.Is(err, apiservercel.ErrCompilation) {
return ValidationCompileError
}
if errors.Is(err, apiservercel.ErrOutOfBudget) {
return ValidatingOutOfBudget
}
return ValidatingInvalidError
}

View File

@ -1,122 +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 metrics
import (
"context"
"time"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
const (
metricsNamespace = "apiserver"
metricsSubsystem = "validating_admission_policy"
)
// ValidationErrorType defines different error types that happen to a validation expression
type ValidationErrorType string
const (
// ValidationCompileError indicates that the expression fails to compile.
ValidationCompileError ValidationErrorType = "compile_error"
// ValidatingInvalidError indicates that the expression fails due to internal
// errors that are out of the control of the user.
ValidatingInvalidError ValidationErrorType = "invalid_error"
// ValidatingOutOfBudget indicates that the expression fails due to running
// out of cost budget, or the budget cannot be obtained.
ValidatingOutOfBudget ValidationErrorType = "out_of_budget"
// ValidationNoError indicates that the expression returns without an error.
ValidationNoError ValidationErrorType = "no_error"
)
var (
// Metrics provides access to validation admission metrics.
Metrics = newValidationAdmissionMetrics()
)
// ValidatingAdmissionPolicyMetrics aggregates Prometheus metrics related to validation admission control.
type ValidatingAdmissionPolicyMetrics struct {
policyCheck *metrics.CounterVec
policyLatency *metrics.HistogramVec
}
func newValidationAdmissionMetrics() *ValidatingAdmissionPolicyMetrics {
check := metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubsystem,
Name: "check_total",
Help: "Validation admission policy check total, labeled by policy and further identified by binding and enforcement action taken.",
StabilityLevel: metrics.BETA,
},
[]string{"policy", "policy_binding", "error_type", "enforcement_action"},
)
latency := metrics.NewHistogramVec(&metrics.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubsystem,
Name: "check_duration_seconds",
Help: "Validation admission latency for individual validation expressions in seconds, labeled by policy and further including binding and enforcement action taken.",
// the bucket distribution here is based oo the benchmark suite at
// github.com/DangerOnTheRanger/cel-benchmark performed on 16-core Intel Xeon
// the lowest bucket was based around the 180ns/op figure for BenchmarkAccess,
// plus some additional leeway to account for the apiserver doing other things
// the largest bucket was chosen based on the fact that benchmarks indicate the
// same Xeon running a CEL expression close to the estimated cost limit takes
// around 760ms, so that bucket should only ever have the slowest CEL expressions
// in it
Buckets: []float64{0.0000005, 0.001, 0.01, 0.1, 1.0},
StabilityLevel: metrics.BETA,
},
[]string{"policy", "policy_binding", "error_type", "enforcement_action"},
)
legacyregistry.MustRegister(check)
legacyregistry.MustRegister(latency)
return &ValidatingAdmissionPolicyMetrics{policyCheck: check, policyLatency: latency}
}
// Reset resets all validation admission-related Prometheus metrics.
func (m *ValidatingAdmissionPolicyMetrics) Reset() {
m.policyCheck.Reset()
m.policyLatency.Reset()
}
// ObserveAdmission observes a policy validation, with an optional error to indicate the error that may occur but ignored.
func (m *ValidatingAdmissionPolicyMetrics) ObserveAdmission(ctx context.Context, elapsed time.Duration, policy, binding string, errorType ValidationErrorType) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, string(errorType), "allow").Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, string(errorType), "allow").Observe(elapsed.Seconds())
}
// ObserveRejection observes a policy validation error that was at least one of the reasons for a deny.
func (m *ValidatingAdmissionPolicyMetrics) ObserveRejection(ctx context.Context, elapsed time.Duration, policy, binding string, errorType ValidationErrorType) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, string(errorType), "deny").Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, string(errorType), "deny").Observe(elapsed.Seconds())
}
// ObserveAudit observes a policy validation audit annotation was published for a validation failure.
func (m *ValidatingAdmissionPolicyMetrics) ObserveAudit(ctx context.Context, elapsed time.Duration, policy, binding string, errorType ValidationErrorType) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, string(errorType), "audit").Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, string(errorType), "audit").Observe(elapsed.Seconds())
}
// ObserveWarn observes a policy validation warning was published for a validation failure.
func (m *ValidatingAdmissionPolicyMetrics) ObserveWarn(ctx context.Context, elapsed time.Duration, policy, binding string, errorType ValidationErrorType) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, string(errorType), "warn").Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, string(errorType), "warn").Observe(elapsed.Seconds())
}

View File

@ -1,211 +0,0 @@
/*
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"
"sync"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/meta"
"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"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
)
const (
// PluginName indicates the name of admission plug-in
PluginName = "ValidatingAdmissionPolicy"
)
var (
lazyCompositionEnvTemplateWithStrictCostInit sync.Once
lazyCompositionEnvTemplateWithStrictCost *cel.CompositionEnv
lazyCompositionEnvTemplateWithoutStrictCostInit sync.Once
lazyCompositionEnvTemplateWithoutStrictCost *cel.CompositionEnv
)
func getCompositionEnvTemplateWithStrictCost() *cel.CompositionEnv {
lazyCompositionEnvTemplateWithStrictCostInit.Do(func() {
env, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
if err != nil {
panic(err)
}
lazyCompositionEnvTemplateWithStrictCost = env
})
return lazyCompositionEnvTemplateWithStrictCost
}
func getCompositionEnvTemplateWithoutStrictCost() *cel.CompositionEnv {
lazyCompositionEnvTemplateWithoutStrictCostInit.Do(func() {
env, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), false))
if err != nil {
panic(err)
}
lazyCompositionEnvTemplateWithoutStrictCost = env
})
return lazyCompositionEnvTemplateWithoutStrictCost
}
// 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 = v1.ValidatingAdmissionPolicy
type PolicyBinding = v1.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.WantsExcludedAdmissionResources = &Plugin{}
func NewPlugin(_ io.Reader) *Plugin {
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
p := &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().V1().ValidatingAdmissionPolicies().Informer(),
f.Admissionregistration().V1().ValidatingAdmissionPolicyBindings().Informer(),
NewValidatingAdmissionPolicyAccessor,
NewValidatingAdmissionPolicyBindingAccessor,
compilePolicy,
f,
dynamicClient,
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, generic.NewPolicyMatcher(m))
},
),
}
p.SetEnabled(true)
return p
}
// 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 compilePolicy(policy *Policy) Validator {
hasParam := false
if policy.Spec.ParamKind != nil {
hasParam = true
}
strictCost := utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP)
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true, StrictCost: strictCost}
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false, StrictCost: strictCost}
failurePolicy := policy.Spec.FailurePolicy
var matcher matchconditions.Matcher = nil
matchConditions := policy.Spec.MatchConditions
var compositionEnvTemplate *cel.CompositionEnv
if strictCost {
compositionEnvTemplate = getCompositionEnvTemplateWithStrictCost()
} else {
compositionEnvTemplate = getCompositionEnvTemplateWithoutStrictCost()
}
filterCompiler := cel.NewCompositedCompilerFromTemplate(compositionEnvTemplate)
filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions)
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(filterCompiler.CompileCondition(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
}
res := NewValidator(
filterCompiler.CompileCondition(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
matcher,
filterCompiler.CompileCondition(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
failurePolicy,
)
return res
}
func convertv1Validations(inputValidations []v1.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 convertv1MessageExpressions(inputValidations []v1.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 convertv1AuditAnnotations(inputValidations []v1.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 []v1.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
}

View File

@ -1,87 +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 (
"net/http"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type PolicyDecisionAction string
const (
ActionAdmit PolicyDecisionAction = "admit"
ActionDeny PolicyDecisionAction = "deny"
)
type PolicyDecisionEvaluation string
const (
EvalAdmit PolicyDecisionEvaluation = "admit"
EvalError PolicyDecisionEvaluation = "error"
EvalDeny PolicyDecisionEvaluation = "deny"
)
// PolicyDecision contains the action determined from a cel evaluation along with metadata such as message, reason and duration
type PolicyDecision struct {
Action PolicyDecisionAction
Evaluation PolicyDecisionEvaluation
Message string
Reason metav1.StatusReason
Elapsed time.Duration
}
type PolicyAuditAnnotationAction string
const (
// AuditAnnotationActionPublish indicates that the audit annotation should be
// published with the audit event.
AuditAnnotationActionPublish PolicyAuditAnnotationAction = "publish"
// AuditAnnotationActionError indicates that the valueExpression resulted
// in an error.
AuditAnnotationActionError PolicyAuditAnnotationAction = "error"
// AuditAnnotationActionExclude indicates that the audit annotation should be excluded
// because the valueExpression evaluated to null, or because FailurePolicy is Ignore
// and the expression failed with a parse error, type check error, or runtime error.
AuditAnnotationActionExclude PolicyAuditAnnotationAction = "exclude"
)
type PolicyAuditAnnotation struct {
Key string
Value string
Elapsed time.Duration
Action PolicyAuditAnnotationAction
Error string
}
func reasonToCode(r metav1.StatusReason) int32 {
switch r {
case metav1.StatusReasonForbidden:
return http.StatusForbidden
case metav1.StatusReasonUnauthorized:
return http.StatusUnauthorized
case metav1.StatusReasonRequestEntityTooLarge:
return http.StatusRequestEntityTooLarge
case metav1.StatusReasonInvalid:
return http.StatusUnprocessableEntity
default:
// It should not reach here since we only allow above reason to be set from API level
return http.StatusUnprocessableEntity
}
}

View File

@ -1,489 +0,0 @@
/*
Copyright 2023 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 (
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/google/cel-go/cel"
"k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/version"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/openapi"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"
)
const maxTypesToCheck = 10
type TypeChecker struct {
SchemaResolver resolver.SchemaResolver
RestMapper meta.RESTMapper
}
// TypeCheckingContext holds information about the policy being type-checked.
// The struct is opaque to the caller.
type TypeCheckingContext struct {
gvks []schema.GroupVersionKind
declTypes []*apiservercel.DeclType
paramGVK schema.GroupVersionKind
paramDeclType *apiservercel.DeclType
variables []v1.Variable
}
type typeOverwrite struct {
object *apiservercel.DeclType
params *apiservercel.DeclType
}
// TypeCheckingResult holds the issues found during type checking, any returned
// error, and the gvk that the type checking is performed against.
type TypeCheckingResult struct {
// GVK is the associated GVK
GVK schema.GroupVersionKind
// Issues contain machine-readable information about the typechecking result.
Issues error
// Err is the possible error that was encounter during type checking.
Err error
}
// TypeCheckingResults is a collection of TypeCheckingResult
type TypeCheckingResults []*TypeCheckingResult
func (rs TypeCheckingResults) String() string {
var messages []string
for _, r := range rs {
message := r.String()
if message != "" {
messages = append(messages, message)
}
}
return strings.Join(messages, "\n")
}
// String converts the result to human-readable form as a string.
func (r *TypeCheckingResult) String() string {
if r.Issues == nil && r.Err == nil {
return ""
}
if r.Err != nil {
return fmt.Sprintf("%v: type checking error: %v\n", r.GVK, r.Err)
}
return fmt.Sprintf("%v: %s\n", r.GVK, r.Issues)
}
// Check preforms the type check against the given policy, and format the result
// as []ExpressionWarning that is ready to be set in policy.Status
// The result is nil if type checking returns no warning.
// The policy object is NOT mutated. The caller should update Status accordingly
func (c *TypeChecker) Check(policy *v1.ValidatingAdmissionPolicy) []v1.ExpressionWarning {
ctx := c.CreateContext(policy)
// warnings to return, note that the capacity is optimistically set to zero
var warnings []v1.ExpressionWarning // intentionally not setting capacity
// check main validation expressions and their message expressions, located in spec.validations[*]
fieldRef := field.NewPath("spec", "validations")
for i, v := range policy.Spec.Validations {
results := c.CheckExpression(ctx, v.Expression)
if len(results) != 0 {
warnings = append(warnings, v1.ExpressionWarning{
FieldRef: fieldRef.Index(i).Child("expression").String(),
Warning: results.String(),
})
}
// Note that MessageExpression is optional
if v.MessageExpression == "" {
continue
}
results = c.CheckExpression(ctx, v.MessageExpression)
if len(results) != 0 {
warnings = append(warnings, v1.ExpressionWarning{
FieldRef: fieldRef.Index(i).Child("messageExpression").String(),
Warning: results.String(),
})
}
}
return warnings
}
// CreateContext resolves all types and their schemas from a policy definition and creates the context.
func (c *TypeChecker) CreateContext(policy *v1.ValidatingAdmissionPolicy) *TypeCheckingContext {
ctx := new(TypeCheckingContext)
allGvks := c.typesToCheck(policy)
gvks := make([]schema.GroupVersionKind, 0, len(allGvks))
declTypes := make([]*apiservercel.DeclType, 0, len(allGvks))
for _, gvk := range allGvks {
declType, err := c.declType(gvk)
if err != nil {
// type checking errors MUST NOT alter the behavior of the policy
// even if an error occurs.
if !errors.Is(err, resolver.ErrSchemaNotFound) {
// Anything except ErrSchemaNotFound is an internal error
klog.V(2).ErrorS(err, "internal error: schema resolution failure", "gvk", gvk)
}
// skip for not found or internal error
continue
}
gvks = append(gvks, gvk)
declTypes = append(declTypes, declType)
}
ctx.gvks = gvks
ctx.declTypes = declTypes
paramsGVK := c.paramsGVK(policy) // maybe empty, correctly handled
paramsDeclType, err := c.declType(paramsGVK)
if err != nil {
if !errors.Is(err, resolver.ErrSchemaNotFound) {
klog.V(2).ErrorS(err, "internal error: cannot resolve schema for params", "gvk", paramsGVK)
}
paramsDeclType = nil
}
ctx.paramGVK = paramsGVK
ctx.paramDeclType = paramsDeclType
ctx.variables = policy.Spec.Variables
return ctx
}
func (c *TypeChecker) compiler(ctx *TypeCheckingContext, typeOverwrite typeOverwrite) (*plugincel.CompositedCompiler, error) {
envSet, err := buildEnvSet(
/* hasParams */ ctx.paramDeclType != nil,
/* hasAuthorizer */ true,
typeOverwrite)
if err != nil {
return nil, err
}
env, err := plugincel.NewCompositionEnv(plugincel.VariablesTypeName, envSet)
if err != nil {
return nil, err
}
compiler := &plugincel.CompositedCompiler{
Compiler: &typeCheckingCompiler{typeOverwrite: typeOverwrite, compositionEnv: env},
CompositionEnv: env,
}
return compiler, nil
}
// CheckExpression type checks a single expression, given the context
func (c *TypeChecker) CheckExpression(ctx *TypeCheckingContext, expression string) TypeCheckingResults {
var results TypeCheckingResults
for i, gvk := range ctx.gvks {
declType := ctx.declTypes[i]
compiler, err := c.compiler(ctx, typeOverwrite{
object: declType,
params: ctx.paramDeclType,
})
if err != nil {
utilruntime.HandleError(err)
continue
}
options := plugincel.OptionalVariableDeclarations{
HasParams: ctx.paramDeclType != nil,
HasAuthorizer: true,
StrictCost: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP),
}
compiler.CompileAndStoreVariables(convertv1beta1Variables(ctx.variables), options, environment.StoredExpressions)
result := compiler.CompileCELExpression(celExpression(expression), options, environment.StoredExpressions)
if err := result.Error; err != nil {
typeCheckingResult := &TypeCheckingResult{GVK: gvk}
if err.Type == apiservercel.ErrorTypeInvalid {
typeCheckingResult.Issues = err
} else {
typeCheckingResult.Err = err
}
results = append(results, typeCheckingResult)
}
}
return results
}
type celExpression string
func (c celExpression) GetExpression() string {
return string(c)
}
func (c celExpression) ReturnTypes() []*cel.Type {
return []*cel.Type{cel.AnyType}
}
func generateUniqueTypeName(kind string) string {
return fmt.Sprintf("%s%d", kind, time.Now().Nanosecond())
}
func (c *TypeChecker) declType(gvk schema.GroupVersionKind) (*apiservercel.DeclType, error) {
if gvk.Empty() {
return nil, nil
}
s, err := c.SchemaResolver.ResolveSchema(gvk)
if err != nil {
return nil, err
}
return common.SchemaDeclType(&openapi.Schema{Schema: s}, true).MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)), nil
}
func (c *TypeChecker) paramsGVK(policy *v1.ValidatingAdmissionPolicy) schema.GroupVersionKind {
if policy.Spec.ParamKind == nil {
return schema.GroupVersionKind{}
}
gv, err := schema.ParseGroupVersion(policy.Spec.ParamKind.APIVersion)
if err != nil {
return schema.GroupVersionKind{}
}
return gv.WithKind(policy.Spec.ParamKind.Kind)
}
// typesToCheck extracts a list of GVKs that needs type checking from the policy
// the result is sorted in the order of Group, Version, and Kind
func (c *TypeChecker) typesToCheck(p *v1.ValidatingAdmissionPolicy) []schema.GroupVersionKind {
gvks := sets.New[schema.GroupVersionKind]()
if p.Spec.MatchConstraints == nil || len(p.Spec.MatchConstraints.ResourceRules) == 0 {
return nil
}
restMapperRefreshAttempted := false // at most once per policy, refresh RESTMapper and retry resolution.
for _, rule := range p.Spec.MatchConstraints.ResourceRules {
groups := extractGroups(&rule.Rule)
if len(groups) == 0 {
continue
}
versions := extractVersions(&rule.Rule)
if len(versions) == 0 {
continue
}
resources := extractResources(&rule.Rule)
if len(resources) == 0 {
continue
}
// sort GVRs so that the loop below provides
// consistent results.
sort.Strings(groups)
sort.Strings(versions)
sort.Strings(resources)
count := 0
for _, group := range groups {
for _, version := range versions {
for _, resource := range resources {
gvr := schema.GroupVersionResource{
Group: group,
Version: version,
Resource: resource,
}
resolved, err := c.RestMapper.KindsFor(gvr)
if err != nil {
if restMapperRefreshAttempted {
// RESTMapper refresh happens at most once per policy
continue
}
c.tryRefreshRESTMapper()
restMapperRefreshAttempted = true
resolved, err = c.RestMapper.KindsFor(gvr)
if err != nil {
continue
}
}
for _, r := range resolved {
if !r.Empty() {
gvks.Insert(r)
count++
// early return if maximum number of types are already
// collected
if count == maxTypesToCheck {
if gvks.Len() == 0 {
return nil
}
return sortGVKList(gvks.UnsortedList())
}
}
}
}
}
}
}
if gvks.Len() == 0 {
return nil
}
return sortGVKList(gvks.UnsortedList())
}
func extractGroups(rule *v1.Rule) []string {
groups := make([]string, 0, len(rule.APIGroups))
for _, group := range rule.APIGroups {
// give up if wildcard
if strings.ContainsAny(group, "*") {
return nil
}
groups = append(groups, group)
}
return groups
}
func extractVersions(rule *v1.Rule) []string {
versions := make([]string, 0, len(rule.APIVersions))
for _, version := range rule.APIVersions {
if strings.ContainsAny(version, "*") {
return nil
}
versions = append(versions, version)
}
return versions
}
func extractResources(rule *v1.Rule) []string {
resources := make([]string, 0, len(rule.Resources))
for _, resource := range rule.Resources {
// skip wildcard and subresources
if strings.ContainsAny(resource, "*/") {
continue
}
resources = append(resources, resource)
}
return resources
}
// sortGVKList sorts the list by Group, Version, and Kind
// returns the list itself.
func sortGVKList(list []schema.GroupVersionKind) []schema.GroupVersionKind {
sort.Slice(list, func(i, j int) bool {
if g := strings.Compare(list[i].Group, list[j].Group); g != 0 {
return g < 0
}
if v := strings.Compare(list[i].Version, list[j].Version); v != 0 {
return v < 0
}
return strings.Compare(list[i].Kind, list[j].Kind) < 0
})
return list
}
// tryRefreshRESTMapper refreshes the RESTMapper if it supports refreshing.
func (c *TypeChecker) tryRefreshRESTMapper() {
if r, ok := c.RestMapper.(meta.ResettableRESTMapper); ok {
r.Reset()
}
}
func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*environment.EnvSet, error) {
baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP))
requestType := plugincel.BuildRequestType()
namespaceType := plugincel.BuildNamespaceType()
var varOpts []cel.EnvOption
var declTypes []*apiservercel.DeclType
// namespace, hand-crafted type
declTypes = append(declTypes, namespaceType)
varOpts = append(varOpts, createVariableOpts(namespaceType, plugincel.NamespaceVarName)...)
// request, hand-crafted type
declTypes = append(declTypes, requestType)
varOpts = append(varOpts, createVariableOpts(requestType, plugincel.RequestVarName)...)
// object and oldObject, same type, type(s) resolved from constraints
declTypes = append(declTypes, types.object)
varOpts = append(varOpts, createVariableOpts(types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)...)
// params, defined by ParamKind
if hasParams && types.params != nil {
declTypes = append(declTypes, types.params)
varOpts = append(varOpts, createVariableOpts(types.params, plugincel.ParamsVarName)...)
}
// authorizer, implicitly available to all expressions of a policy
if hasAuthorizer {
// we only need its structure but not the variable itself
varOpts = append(varOpts, cel.Variable("authorizer", library.AuthorizerType))
}
return baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: varOpts,
DeclTypes: declTypes,
},
)
}
// createVariableOpts creates a slice of EnvOption
// that can be used for creating a CEL env containing variables of declType.
// declType can be nil, in which case the variables will be of DynType.
func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption {
opts := make([]cel.EnvOption, 0, len(variables))
t := cel.DynType
if declType != nil {
t = declType.CelType()
}
for _, v := range variables {
opts = append(opts, cel.Variable(v, t))
}
return opts
}
type typeCheckingCompiler struct {
compositionEnv *plugincel.CompositionEnv
typeOverwrite typeOverwrite
}
// CompileCELExpression compiles the given expression.
// The implementation is the same as that of staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go
// except that:
// - object, oldObject, and params are typed instead of Dyn
// - compiler does not enforce the output type
// - the compiler does not initialize the program
func (c *typeCheckingCompiler) CompileCELExpression(expressionAccessor plugincel.ExpressionAccessor, options plugincel.OptionalVariableDeclarations, mode environment.Type) plugincel.CompilationResult {
resultError := func(errorString string, errType apiservercel.ErrorType) plugincel.CompilationResult {
return plugincel.CompilationResult{
Error: &apiservercel.Error{
Type: errType,
Detail: errorString,
},
ExpressionAccessor: expressionAccessor,
}
}
env, err := c.compositionEnv.Env(mode)
if err != nil {
return resultError(fmt.Sprintf("fail to build env: %v", err), apiservercel.ErrorTypeInternal)
}
ast, issues := env.Compile(expressionAccessor.GetExpression())
if issues != nil {
return resultError(issues.String(), apiservercel.ErrorTypeInvalid)
}
// type checker does not require the program, however the type must still be set.
return plugincel.CompilationResult{
OutputType: ast.OutputType(),
}
}
var _ plugincel.Compiler = (*typeCheckingCompiler)(nil)

View File

@ -1,249 +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"
"strings"
celtypes "github.com/google/cel-go/common/types"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/klog/v2"
)
// validator implements the Validator interface
type validator struct {
celMatcher matchconditions.Matcher
validationFilter cel.ConditionEvaluator
auditAnnotationFilter cel.ConditionEvaluator
messageFilter cel.ConditionEvaluator
failPolicy *v1.FailurePolicyType
}
func NewValidator(validationFilter cel.ConditionEvaluator, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.ConditionEvaluator, failPolicy *v1.FailurePolicyType) Validator {
return &validator{
celMatcher: celMatcher,
validationFilter: validationFilter,
auditAnnotationFilter: auditAnnotationFilter,
messageFilter: messageFilter,
failPolicy: failPolicy,
}
}
func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction {
if f == v1.Ignore {
return ActionAdmit
}
return ActionDeny
}
func auditAnnotationEvaluationForError(f v1.FailurePolicyType) PolicyAuditAnnotationAction {
if f == v1.Ignore {
return AuditAnnotationActionExclude
}
return AuditAnnotationActionError
}
// Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
var f v1.FailurePolicyType
if v.failPolicy == nil {
f = v1.Fail
} else {
f = *v.failPolicy
}
if v.celMatcher != nil {
matchResults := v.celMatcher.Match(ctx, versionedAttr, versionedParams, authz)
if matchResults.Error != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: matchResults.Error.Error(),
},
},
}
}
// if preconditions are not met, then do not return any validations
if !matchResults.Matches {
return ValidateResult{}
}
}
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: authz}
expressionOptionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams}
admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(matchedResource), metav1.GroupVersionKind(versionedAttr.VersionedKind))
// Decide which fields are exposed
ns := cel.CreateNamespaceObject(namespace)
evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, ns, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
},
}
}
decisions := make([]PolicyDecision, len(evalResults))
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, ns, remainingBudget)
for i, evalResult := range evalResults {
var decision = &decisions[i]
decision.Elapsed = evalResult.Elapsed
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition)
if !ok {
klog.Error("Invalid type conversion to ValidationCondition")
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = "Invalid type sent to validator, expected ValidationCondition"
continue
}
var messageResult *cel.EvaluationResult
if len(messageResults) > i {
messageResult = &messageResults[i]
}
if evalResult.Error != nil {
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = evalResult.Error.Error()
} else if errors.Is(err, apiservercel.ErrInternal) || errors.Is(err, apiservercel.ErrOutOfBudget) {
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = fmt.Sprintf("failed messageExpression: %s", err)
} else if evalResult.EvalResult != celtypes.True {
decision.Action = ActionDeny
decision.Evaluation = EvalDeny
if validation.Reason == nil {
decision.Reason = metav1.StatusReasonInvalid
} else {
decision.Reason = *validation.Reason
}
// decide the failure message
var message string
// attempt to set message with messageExpression result
if messageResult != nil && messageResult.Error == nil && messageResult.EvalResult != nil {
// also fallback if the eval result is non-string (including null) or
// whitespaces.
if message, ok = messageResult.EvalResult.Value().(string); ok {
message = strings.TrimSpace(message)
// deny excessively long message from EvalResult
if len(message) > celconfig.MaxEvaluatedMessageExpressionSizeBytes {
klog.V(2).InfoS("excessively long message denied", "message", message)
message = ""
}
// deny message that contains newlines
if strings.ContainsAny(message, "\n") {
klog.V(2).InfoS("multi-line message denied", "message", message)
message = ""
}
}
}
if messageResult != nil && messageResult.Error != nil {
// log any error with messageExpression
klog.V(2).ErrorS(messageResult.Error, "error while evaluating messageExpression")
}
// fallback to set message to the custom message
if message == "" && len(validation.Message) > 0 {
message = strings.TrimSpace(validation.Message)
}
// fallback to use the expression to compose a message
if message == "" {
message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
}
decision.Message = message
} else {
decision.Action = ActionAdmit
decision.Evaluation = EvalAdmit
}
}
options := cel.OptionalVariableBindings{VersionedParams: versionedParams}
auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, admissionRequest, options, namespace, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
},
}
}
auditAnnotationResults := make([]PolicyAuditAnnotation, len(auditAnnotationEvalResults))
for i, evalResult := range auditAnnotationEvalResults {
if evalResult.ExpressionAccessor == nil {
continue
}
var auditAnnotationResult = &auditAnnotationResults[i]
auditAnnotationResult.Elapsed = evalResult.Elapsed
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
if !ok {
klog.Error("Invalid type conversion to AuditAnnotationCondition")
auditAnnotationResult.Action = auditAnnotationEvaluationForError(f)
auditAnnotationResult.Error = fmt.Sprintf("Invalid type sent to validator, expected AuditAnnotationCondition but got %T", evalResult.ExpressionAccessor)
continue
}
auditAnnotationResult.Key = validation.Key
if evalResult.Error != nil {
auditAnnotationResult.Action = auditAnnotationEvaluationForError(f)
auditAnnotationResult.Error = evalResult.Error.Error()
} else {
switch evalResult.EvalResult.Type() {
case celtypes.StringType:
value := strings.TrimSpace(evalResult.EvalResult.Value().(string))
if len(value) == 0 {
auditAnnotationResult.Action = AuditAnnotationActionExclude
} else {
auditAnnotationResult.Action = AuditAnnotationActionPublish
auditAnnotationResult.Value = value
}
case celtypes.NullType:
auditAnnotationResult.Action = AuditAnnotationActionExclude
default:
auditAnnotationResult.Action = AuditAnnotationActionError
auditAnnotationResult.Error = fmt.Sprintf("valueExpression '%v' resulted in unsupported return type: %v. "+
"Return type must be either string or null.", validation.ValueExpression, evalResult.EvalResult.Type())
}
}
}
return ValidateResult{Decisions: decisions, AuditAnnotations: auditAnnotationResults}
}

View File

@ -1,333 +0,0 @@
/*
Copyright 2018 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"
"sync"
"time"
"go.opentelemetry.io/otel/attribute"
v1 "k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
webhookrequest "k8s.io/apiserver/pkg/admission/plugin/webhook/request"
endpointsrequest "k8s.io/apiserver/pkg/endpoints/request"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/pkg/warning"
"k8s.io/component-base/tracing"
"k8s.io/klog/v2"
)
const (
// ValidatingAuditAnnotationPrefix is a prefix for keeping noteworthy
// validating audit annotations.
ValidatingAuditAnnotationPrefix = "validating.webhook.admission.k8s.io/"
// ValidatingAuditAnnotationFailedOpenKeyPrefix in an annotation indicates
// the validating webhook failed open when the webhook backend connection
// failed or returned an internal server error.
ValidatingAuditAnnotationFailedOpenKeyPrefix = "failed-open." + ValidatingAuditAnnotationPrefix
)
type validatingDispatcher struct {
cm *webhookutil.ClientManager
plugin *Plugin
}
func newValidatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher {
return func(cm *webhookutil.ClientManager) generic.Dispatcher {
return &validatingDispatcher{cm, p}
}
}
var _ generic.VersionedAttributeAccessor = &versionedAttributeAccessor{}
type versionedAttributeAccessor struct {
versionedAttrs map[schema.GroupVersionKind]*admission.VersionedAttributes
attr admission.Attributes
objectInterfaces admission.ObjectInterfaces
}
func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
if val, ok := v.versionedAttrs[gvk]; ok {
return val, nil
}
versionedAttr, err := admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces)
if err != nil {
return nil, err
}
v.versionedAttrs[gvk] = versionedAttr
return versionedAttr, nil
}
var _ generic.Dispatcher = &validatingDispatcher{}
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
var relevantHooks []*generic.WebhookInvocation
// Construct all the versions we need to call our webhooks
versionedAttrAccessor := &versionedAttributeAccessor{
versionedAttrs: map[schema.GroupVersionKind]*admission.VersionedAttributes{},
attr: attr,
objectInterfaces: o,
}
for _, hook := range hooks {
invocation, statusError := d.plugin.ShouldCallHook(ctx, hook, attr, o, versionedAttrAccessor)
if statusError != nil {
return statusError
}
if invocation == nil {
continue
}
relevantHooks = append(relevantHooks, invocation)
// VersionedAttr result will be cached and reused later during parallel webhook calls
_, err := versionedAttrAccessor.VersionedAttribute(invocation.Kind)
if err != nil {
return apierrors.NewInternalError(err)
}
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
}
// Check if the request has already timed out before spawning remote calls
select {
case <-ctx.Done():
// parent context is canceled or timed out, no point in continuing
return apierrors.NewTimeoutError("request did not complete within requested timeout", 0)
default:
}
wg := sync.WaitGroup{}
errCh := make(chan error, 2*len(relevantHooks)) // double the length to handle extra errors for panics in the gofunc
wg.Add(len(relevantHooks))
for i := range relevantHooks {
go func(invocation *generic.WebhookInvocation, idx int) {
ignoreClientCallFailures := false
hookName := "unknown"
versionedAttr := versionedAttrAccessor.versionedAttrs[invocation.Kind]
// The ordering of these two defers is critical. The wg.Done will release the parent go func to close the errCh
// that is used by the second defer to report errors. The recovery and error reporting must be done first.
defer wg.Done()
defer func() {
// HandleCrash has already called the crash handlers and it has been configured to utilruntime.ReallyCrash
// This block prevents the second panic from failing our process.
// This failure mode for the handler functions properly using the channel below.
recover()
}()
defer utilruntime.HandleCrash(
func(r interface{}) {
if r == nil {
return
}
if ignoreClientCallFailures {
// if failures are supposed to ignored, ignore it
klog.Warningf("Panic calling webhook, failing open %v: %v", hookName, r)
admissionmetrics.Metrics.ObserveWebhookFailOpen(ctx, hookName, "validating")
key := fmt.Sprintf("%sround_0_index_%d", ValidatingAuditAnnotationFailedOpenKeyPrefix, idx)
value := hookName
if err := versionedAttr.Attributes.AddAnnotation(key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, value, hookName, err)
}
return
}
// this ensures that the admission request fails and a message is provided.
errCh <- apierrors.NewInternalError(fmt.Errorf("ValidatingAdmissionWebhook/%v has panicked: %v", hookName, r))
},
)
hook, ok := invocation.Webhook.GetValidatingWebhook()
if !ok {
utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1.ValidatingWebhook, but got %T", hook))
return
}
hookName = hook.Name
ignoreClientCallFailures = hook.FailurePolicy != nil && *hook.FailurePolicy == v1.Ignore
t := time.Now()
err := d.callHook(ctx, hook, invocation, versionedAttr)
rejected := false
if err != nil {
switch err := err.(type) {
case *webhookutil.ErrCallingWebhook:
if !ignoreClientCallFailures {
rejected = true
// Ignore context cancelled from webhook metrics
if !errors.Is(err.Reason, context.Canceled) {
admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionCallingWebhookError, int(err.Status.ErrStatus.Code))
}
}
admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", int(err.Status.ErrStatus.Code))
case *webhookutil.ErrWebhookRejection:
rejected = true
admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionNoError, int(err.Status.ErrStatus.Code))
admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", int(err.Status.ErrStatus.Code))
default:
rejected = true
admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionAPIServerInternalError, 0)
admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", 0)
}
} else {
admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", 200)
return
}
if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
if ignoreClientCallFailures {
// Ignore context cancelled from webhook metrics
if errors.Is(callErr.Reason, context.Canceled) {
klog.Warningf("Context canceled when calling webhook %v", hook.Name)
} else {
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
admissionmetrics.Metrics.ObserveWebhookFailOpen(ctx, hook.Name, "validating")
key := fmt.Sprintf("%sround_0_index_%d", ValidatingAuditAnnotationFailedOpenKeyPrefix, idx)
value := hook.Name
if err := versionedAttr.Attributes.AddAnnotation(key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, value, hook.Name, err)
}
}
utilruntime.HandleError(callErr)
return
}
klog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
errCh <- apierrors.NewInternalError(err)
return
}
if rejectionErr, ok := err.(*webhookutil.ErrWebhookRejection); ok {
err = rejectionErr.Status
}
klog.Warningf("rejected by webhook %q: %#v", hook.Name, err)
errCh <- err
}(relevantHooks[i], i)
}
wg.Wait()
close(errCh)
var errs []error
for e := range errCh {
errs = append(errs, e)
}
if len(errs) == 0 {
return nil
}
if len(errs) > 1 {
for i := 1; i < len(errs); i++ {
// TODO: merge status errors; until then, just return the first one.
utilruntime.HandleError(errs[i])
}
}
return errs[0]
}
func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *admission.VersionedAttributes) error {
if attr.Attributes.IsDryRun() {
if h.SideEffects == nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil"), Status: apierrors.NewBadRequest("Webhook SideEffects is nil")}
}
if !(*h.SideEffects == v1.SideEffectClassNone || *h.SideEffects == v1.SideEffectClassNoneOnDryRun) {
return webhookerrors.NewDryRunUnsupportedErr(h.Name)
}
}
uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation)
if err != nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not create admission objects: %w", err), Status: apierrors.NewBadRequest("error creating admission objects")}
}
// Make the webhook request
client, err := invocation.Webhook.GetRESTClient(d.cm)
if err != nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not get REST client: %w", err), Status: apierrors.NewBadRequest("error getting REST client")}
}
ctx, span := tracing.Start(ctx, "Call validating webhook",
attribute.String("configuration", invocation.Webhook.GetConfigurationName()),
attribute.String("webhook", h.Name),
attribute.Stringer("resource", attr.GetResource()),
attribute.String("subresource", attr.GetSubresource()),
attribute.String("operation", string(attr.GetOperation())),
attribute.String("UID", string(uid)))
defer span.End(500 * time.Millisecond)
// if the webhook has a specific timeout, wrap the context to apply it
if h.TimeoutSeconds != nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(*h.TimeoutSeconds)*time.Second)
defer cancel()
}
r := client.Post().Body(request)
// if the context has a deadline, set it as a parameter to inform the backend
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
// compute the timeout
if timeout := time.Until(deadline); timeout > 0 {
// if it's not an even number of seconds, round up to the nearest second
if truncated := timeout.Truncate(time.Second); truncated != timeout {
timeout = truncated + time.Second
}
// set the timeout
r.Timeout(timeout)
}
}
do := func() { err = r.Do(ctx).Into(response) }
if wd, ok := endpointsrequest.LatencyTrackersFrom(ctx); ok {
tmp := do
do = func() { wd.ValidatingWebhookTracker.Track(tmp) }
}
do()
if err != nil {
var status *apierrors.StatusError
if se, ok := err.(*apierrors.StatusError); ok {
status = se
} else {
status = apierrors.NewBadRequest("error calling webhook")
}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("failed to call webhook: %w", err), Status: status}
}
span.AddEvent("Request completed")
result, err := webhookrequest.VerifyAdmissionResponse(uid, false, response)
if err != nil {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("received invalid webhook response: %w", err), Status: apierrors.NewServiceUnavailable("error validating webhook response")}
}
for k, v := range result.AuditAnnotations {
key := h.Name + "/" + k
if err := attr.Attributes.AddAnnotation(key, v); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, v, h.Name, err)
}
}
for _, w := range result.Warnings {
warning.AddWarning(ctx, "", w)
}
if result.Allowed {
return nil
}
return &webhookutil.ErrWebhookRejection{Status: webhookerrors.ToStatusErr(h.Name, result.Result)}
}

View File

@ -1,19 +0,0 @@
/*
Copyright 2017 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 makes calls to validating (i.e., non-mutating) webhooks
// during the admission process.
package validating

View File

@ -1,67 +0,0 @@
/*
Copyright 2017 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"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/configuration"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
const (
// PluginName indicates the name of admission plug-in
PluginName = "ValidatingAdmissionWebhook"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
plugin, err := NewValidatingAdmissionWebhook(configFile)
if err != nil {
return nil, err
}
return plugin, nil
})
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*generic.Webhook
}
var _ admission.ValidationInterface = &Plugin{}
// NewValidatingAdmissionWebhook returns a generic admission webhook plugin.
func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) {
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
p := &Plugin{}
var err error
p.Webhook, err = generic.NewWebhook(handler, configFile, configuration.NewValidatingWebhookConfigurationManager, newValidatingDispatcher(p))
if err != nil {
return nil, err
}
return p, nil
}
// 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.Webhook.Dispatch(ctx, attr, o)
}

View File

@ -1,226 +0,0 @@
/*
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.
*/
// This file was duplicated from the auto-generated file by conversion-gen in
// k8s.io/kubernetes/pkg/apis/apidiscovery Unlike most k8s types discovery is
// served by all apiservers and conversion is needed by all apiservers. The
// concept of internal/hub type does not exist for discovery as we work directly
// with the versioned types.
// The conversion code here facilities conversion strictly between v2beta1 and
// v2 types. It is only necessary in k8s versions where mixed state could be
// possible before the full removal of the v2beta1 types. It is placed in this
// directory such that all apiservers can benefit from the conversion without
// having to implement their own if the client/server they're communicating with
// only supports one version.
// Once the v2beta1 types are removed (intended for Kubernetes v1.33), this file
// will be removed.
package v2
import (
unsafe "unsafe"
v2 "k8s.io/api/apidiscovery/v2"
v2beta1 "k8s.io/api/apidiscovery/v2beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(s *runtime.Scheme) error {
if err := s.AddGeneratedConversionFunc((*v2beta1.APIGroupDiscovery)(nil), (*v2.APIGroupDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery(a.(*v2beta1.APIGroupDiscovery), b.(*v2.APIGroupDiscovery), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2.APIGroupDiscovery)(nil), (*v2beta1.APIGroupDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery(a.(*v2.APIGroupDiscovery), b.(*v2beta1.APIGroupDiscovery), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2beta1.APIGroupDiscoveryList)(nil), (*v2.APIGroupDiscoveryList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList(a.(*v2beta1.APIGroupDiscoveryList), b.(*v2.APIGroupDiscoveryList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2.APIGroupDiscoveryList)(nil), (*v2beta1.APIGroupDiscoveryList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(a.(*v2.APIGroupDiscoveryList), b.(*v2beta1.APIGroupDiscoveryList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2beta1.APIResourceDiscovery)(nil), (*v2.APIResourceDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery(a.(*v2beta1.APIResourceDiscovery), b.(*v2.APIResourceDiscovery), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2.APIResourceDiscovery)(nil), (*v2beta1.APIResourceDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery(a.(*v2.APIResourceDiscovery), b.(*v2beta1.APIResourceDiscovery), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2beta1.APISubresourceDiscovery)(nil), (*v2.APISubresourceDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery(a.(*v2beta1.APISubresourceDiscovery), b.(*v2.APISubresourceDiscovery), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2.APISubresourceDiscovery)(nil), (*v2beta1.APISubresourceDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery(a.(*v2.APISubresourceDiscovery), b.(*v2beta1.APISubresourceDiscovery), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2beta1.APIVersionDiscovery)(nil), (*v2.APIVersionDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery(a.(*v2beta1.APIVersionDiscovery), b.(*v2.APIVersionDiscovery), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v2.APIVersionDiscovery)(nil), (*v2beta1.APIVersionDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery(a.(*v2.APIVersionDiscovery), b.(*v2beta1.APIVersionDiscovery), scope)
}); err != nil {
return err
}
return nil
}
func autoConvertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery(in *v2beta1.APIGroupDiscovery, out *v2.APIGroupDiscovery, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Versions = *(*[]v2.APIVersionDiscovery)(unsafe.Pointer(&in.Versions))
return nil
}
// Convertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery is an autogenerated conversion function.
func Convertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery(in *v2beta1.APIGroupDiscovery, out *v2.APIGroupDiscovery, s conversion.Scope) error {
return autoConvertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery(in, out, s)
}
func autoConvertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery(in *v2.APIGroupDiscovery, out *v2beta1.APIGroupDiscovery, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Versions = *(*[]v2beta1.APIVersionDiscovery)(unsafe.Pointer(&in.Versions))
return nil
}
// Convertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery is an autogenerated conversion function.
func Convertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery(in *v2.APIGroupDiscovery, out *v2beta1.APIGroupDiscovery, s conversion.Scope) error {
return autoConvertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery(in, out, s)
}
func autoConvertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList(in *v2beta1.APIGroupDiscoveryList, out *v2.APIGroupDiscoveryList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
out.Items = *(*[]v2.APIGroupDiscovery)(unsafe.Pointer(&in.Items))
return nil
}
// Convertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList is an autogenerated conversion function.
func Convertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList(in *v2beta1.APIGroupDiscoveryList, out *v2.APIGroupDiscoveryList, s conversion.Scope) error {
return autoConvertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList(in, out, s)
}
func autoConvertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(in *v2.APIGroupDiscoveryList, out *v2beta1.APIGroupDiscoveryList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
out.Items = *(*[]v2beta1.APIGroupDiscovery)(unsafe.Pointer(&in.Items))
return nil
}
// Convertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList is an autogenerated conversion function.
func Convertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(in *v2.APIGroupDiscoveryList, out *v2beta1.APIGroupDiscoveryList, s conversion.Scope) error {
return autoConvertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(in, out, s)
}
func autoConvertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery(in *v2beta1.APIResourceDiscovery, out *v2.APIResourceDiscovery, s conversion.Scope) error {
out.Resource = in.Resource
out.ResponseKind = (*v1.GroupVersionKind)(unsafe.Pointer(in.ResponseKind))
out.Scope = v2.ResourceScope(in.Scope)
out.SingularResource = in.SingularResource
out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs))
out.ShortNames = *(*[]string)(unsafe.Pointer(&in.ShortNames))
out.Categories = *(*[]string)(unsafe.Pointer(&in.Categories))
out.Subresources = *(*[]v2.APISubresourceDiscovery)(unsafe.Pointer(&in.Subresources))
return nil
}
// Convertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery is an autogenerated conversion function.
func Convertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery(in *v2beta1.APIResourceDiscovery, out *v2.APIResourceDiscovery, s conversion.Scope) error {
return autoConvertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery(in, out, s)
}
func autoConvertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery(in *v2.APIResourceDiscovery, out *v2beta1.APIResourceDiscovery, s conversion.Scope) error {
out.Resource = in.Resource
out.ResponseKind = (*v1.GroupVersionKind)(unsafe.Pointer(in.ResponseKind))
out.Scope = v2beta1.ResourceScope(in.Scope)
out.SingularResource = in.SingularResource
out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs))
out.ShortNames = *(*[]string)(unsafe.Pointer(&in.ShortNames))
out.Categories = *(*[]string)(unsafe.Pointer(&in.Categories))
out.Subresources = *(*[]v2beta1.APISubresourceDiscovery)(unsafe.Pointer(&in.Subresources))
return nil
}
// Convertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery is an autogenerated conversion function.
func Convertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery(in *v2.APIResourceDiscovery, out *v2beta1.APIResourceDiscovery, s conversion.Scope) error {
return autoConvertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery(in, out, s)
}
func autoConvertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery(in *v2beta1.APISubresourceDiscovery, out *v2.APISubresourceDiscovery, s conversion.Scope) error {
out.Subresource = in.Subresource
out.ResponseKind = (*v1.GroupVersionKind)(unsafe.Pointer(in.ResponseKind))
out.AcceptedTypes = *(*[]v1.GroupVersionKind)(unsafe.Pointer(&in.AcceptedTypes))
out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs))
return nil
}
// Convertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery is an autogenerated conversion function.
func Convertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery(in *v2beta1.APISubresourceDiscovery, out *v2.APISubresourceDiscovery, s conversion.Scope) error {
return autoConvertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery(in, out, s)
}
func autoConvertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery(in *v2.APISubresourceDiscovery, out *v2beta1.APISubresourceDiscovery, s conversion.Scope) error {
out.Subresource = in.Subresource
out.ResponseKind = (*v1.GroupVersionKind)(unsafe.Pointer(in.ResponseKind))
out.AcceptedTypes = *(*[]v1.GroupVersionKind)(unsafe.Pointer(&in.AcceptedTypes))
out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs))
return nil
}
// Convertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery is an autogenerated conversion function.
func Convertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery(in *v2.APISubresourceDiscovery, out *v2beta1.APISubresourceDiscovery, s conversion.Scope) error {
return autoConvertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery(in, out, s)
}
func autoConvertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery(in *v2beta1.APIVersionDiscovery, out *v2.APIVersionDiscovery, s conversion.Scope) error {
out.Version = in.Version
out.Resources = *(*[]v2.APIResourceDiscovery)(unsafe.Pointer(&in.Resources))
out.Freshness = v2.DiscoveryFreshness(in.Freshness)
return nil
}
// Convertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery is an autogenerated conversion function.
func Convertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery(in *v2beta1.APIVersionDiscovery, out *v2.APIVersionDiscovery, s conversion.Scope) error {
return autoConvertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery(in, out, s)
}
func autoConvertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery(in *v2.APIVersionDiscovery, out *v2beta1.APIVersionDiscovery, s conversion.Scope) error {
out.Version = in.Version
out.Resources = *(*[]v2beta1.APIResourceDiscovery)(unsafe.Pointer(&in.Resources))
out.Freshness = v2beta1.DiscoveryFreshness(in.Freshness)
return nil
}
// Convertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery is an autogenerated conversion function.
func Convertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery(in *v2.APIVersionDiscovery, out *v2beta1.APIVersionDiscovery, s conversion.Scope) error {
return autoConvertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery(in, out, s)
}

View File

@ -1,19 +0,0 @@
/*
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.
*/
// +groupName=apidiscovery.k8s.io
package v2

View File

@ -1,39 +0,0 @@
/*
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 v2
import (
apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name use in this package
const GroupName = "apidiscovery.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v2"}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
SchemeBuilder = &apidiscoveryv2.SchemeBuilder
// AddToScheme adds api to a scheme
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@ -1,821 +0,0 @@
/*
Copyright 2023 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 validation
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"time"
celgo "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/operators"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
v1 "k8s.io/api/authorization/v1"
"k8s.io/api/authorization/v1beta1"
"k8s.io/apimachinery/pkg/util/sets"
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/apiserver/pkg/apis/apiserver"
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/util/cert"
)
// ValidateAuthenticationConfiguration validates a given AuthenticationConfiguration.
func ValidateAuthenticationConfiguration(compiler authenticationcel.Compiler, c *api.AuthenticationConfiguration, disallowedIssuers []string) field.ErrorList {
root := field.NewPath("jwt")
var allErrs field.ErrorList
// We allow 0 authenticators in the authentication configuration.
// This allows us to support scenarios where the API server is initially set up without
// any authenticators and then authenticators are added later via dynamic config.
if len(c.JWT) > 64 {
allErrs = append(allErrs, field.TooMany(root, len(c.JWT), 64))
return allErrs
}
seenIssuers := sets.New[string]()
seenDiscoveryURLs := sets.New[string]()
for i, a := range c.JWT {
fldPath := root.Index(i)
_, errs := validateJWTAuthenticator(compiler, a, fldPath, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
allErrs = append(allErrs, errs...)
if seenIssuers.Has(a.Issuer.URL) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("issuer").Child("url"), a.Issuer.URL))
}
seenIssuers.Insert(a.Issuer.URL)
if len(a.Issuer.DiscoveryURL) > 0 {
if seenDiscoveryURLs.Has(a.Issuer.DiscoveryURL) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("issuer").Child("discoveryURL"), a.Issuer.DiscoveryURL))
}
seenDiscoveryURLs.Insert(a.Issuer.DiscoveryURL)
}
}
if c.Anonymous != nil {
if !utilfeature.DefaultFeatureGate.Enabled(features.AnonymousAuthConfigurableEndpoints) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("anonymous"), "anonymous is not supported when AnonymousAuthConfigurableEnpoints feature gate is disabled"))
}
if !c.Anonymous.Enabled && len(c.Anonymous.Conditions) > 0 {
allErrs = append(allErrs, field.Invalid(field.NewPath("anonymous", "conditions"), c.Anonymous.Conditions, "enabled should be set to true when conditions are defined"))
}
}
return allErrs
}
// CompileAndValidateJWTAuthenticator validates a given JWTAuthenticator and returns a CELMapper with the compiled
// CEL expressions for claim mappings and validation rules.
// This is exported for use in oidc package.
func CompileAndValidateJWTAuthenticator(compiler authenticationcel.Compiler, authenticator api.JWTAuthenticator, disallowedIssuers []string) (authenticationcel.CELMapper, field.ErrorList) {
return validateJWTAuthenticator(compiler, authenticator, nil, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
}
func validateJWTAuthenticator(compiler authenticationcel.Compiler, authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) {
var allErrs field.ErrorList
state := &validationState{}
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateClaimValidationRules(compiler, state, authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateClaimMappings(compiler, state, authenticator.ClaimMappings, fldPath.Child("claimMappings"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateUserValidationRules(compiler, state, authenticator.UserValidationRules, fldPath.Child("userValidationRules"), structuredAuthnFeatureEnabled)...)
return state.mapper, allErrs
}
type validationState struct {
mapper authenticationcel.CELMapper
usesEmailClaim bool
usesEmailVerifiedClaim bool
}
func validateIssuer(issuer api.Issuer, disallowedIssuers sets.Set[string], fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateIssuerURL(issuer.URL, disallowedIssuers, fldPath.Child("url"))...)
allErrs = append(allErrs, validateIssuerDiscoveryURL(issuer.URL, issuer.DiscoveryURL, fldPath.Child("discoveryURL"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"), structuredAuthnFeatureEnabled)...)
allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...)
return allErrs
}
func validateIssuerURL(issuerURL string, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList {
if len(issuerURL) == 0 {
return field.ErrorList{field.Required(fldPath, "URL is required")}
}
return validateURL(issuerURL, disallowedIssuers, fldPath)
}
func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
if len(issuerDiscoveryURL) == 0 {
return nil
}
if !structuredAuthnFeatureEnabled {
allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
if len(issuerURL) > 0 && strings.TrimRight(issuerURL, "/") == strings.TrimRight(issuerDiscoveryURL, "/") {
allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL must be different from URL"))
}
// issuerDiscoveryURL is not an issuer URL and does not need to validated against any set of disallowed issuers
allErrs = append(allErrs, validateURL(issuerDiscoveryURL, nil, fldPath)...)
return allErrs
}
func validateURL(issuerURL string, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if disallowedIssuers.Has(issuerURL) {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, fmt.Sprintf("URL must not overlap with disallowed issuers: %s", sets.List(disallowedIssuers))))
}
u, err := url.Parse(issuerURL)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error()))
return allErrs
}
if u.Scheme != "https" {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL scheme must be https"))
}
if u.User != nil {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a username or password"))
}
if len(u.RawQuery) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a query"))
}
if len(u.Fragment) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, "URL must not contain a fragment"))
}
return allErrs
}
func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatchPolicyType, fldPath, audienceMatchPolicyFldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
if len(audiences) == 0 {
allErrs = append(allErrs, field.Required(fldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, fldPath)))
return allErrs
}
if len(audiences) > 1 && !structuredAuthnFeatureEnabled {
allErrs = append(allErrs, field.Invalid(fldPath, audiences, "multiple audiences are not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
seenAudiences := sets.NewString()
for i, audience := range audiences {
fldPath := fldPath.Index(i)
if len(audience) == 0 {
allErrs = append(allErrs, field.Required(fldPath, "audience can't be empty"))
}
if seenAudiences.Has(audience) {
allErrs = append(allErrs, field.Duplicate(fldPath, audience))
}
seenAudiences.Insert(audience)
}
if len(audiences) > 1 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny {
allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be MatchAny for multiple audiences"))
}
if len(audiences) == 1 && (len(audienceMatchPolicy) > 0 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny) {
allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be empty or MatchAny for single audience"))
}
return allErrs
}
func validateCertificateAuthority(certificateAuthority string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if len(certificateAuthority) == 0 {
return allErrs
}
_, err := cert.NewPoolFromBytes([]byte(certificateAuthority))
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, "<omitted>", err.Error()))
}
return allErrs
}
func validateClaimValidationRules(compiler authenticationcel.Compiler, state *validationState, rules []api.ClaimValidationRule, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
seenClaims := sets.NewString()
seenExpressions := sets.NewString()
var compilationResults []authenticationcel.CompilationResult
for i, rule := range rules {
fldPath := fldPath.Index(i)
if len(rule.Expression) > 0 && !structuredAuthnFeatureEnabled {
allErrs = append(allErrs, field.Invalid(fldPath.Child("expression"), rule.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
switch {
case len(rule.Claim) > 0 && len(rule.Expression) > 0:
allErrs = append(allErrs, field.Invalid(fldPath, rule.Claim, "claim and expression can't both be set"))
case len(rule.Claim) == 0 && len(rule.Expression) == 0:
allErrs = append(allErrs, field.Required(fldPath, "claim or expression is required"))
case len(rule.Claim) > 0:
if len(rule.Message) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("message"), rule.Message, "message can't be set when claim is set"))
}
if seenClaims.Has(rule.Claim) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("claim"), rule.Claim))
}
seenClaims.Insert(rule.Claim)
case len(rule.Expression) > 0:
if len(rule.RequiredValue) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("requiredValue"), rule.RequiredValue, "requiredValue can't be set when expression is set"))
}
if seenExpressions.Has(rule.Expression) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("expression"), rule.Expression))
continue
}
seenExpressions.Insert(rule.Expression)
compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ClaimValidationCondition{
Expression: rule.Expression,
Message: rule.Message,
}, fldPath.Child("expression"))
if err != nil {
allErrs = append(allErrs, err)
continue
}
if compilationResult != nil {
compilationResults = append(compilationResults, *compilationResult)
}
}
}
if structuredAuthnFeatureEnabled && len(compilationResults) > 0 {
state.mapper.ClaimValidationRules = authenticationcel.NewClaimsMapper(compilationResults)
state.usesEmailVerifiedClaim = state.usesEmailVerifiedClaim || anyUsesEmailVerifiedClaim(compilationResults)
}
return allErrs
}
func validateClaimMappings(compiler authenticationcel.Compiler, state *validationState, m api.ClaimMappings, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
if !structuredAuthnFeatureEnabled {
if len(m.Username.Expression) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("username").Child("expression"), m.Username.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
if len(m.Groups.Expression) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("groups").Child("expression"), m.Groups.Expression, "expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
if len(m.UID.Claim) > 0 || len(m.UID.Expression) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), "", "uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
if len(m.Extra) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("extra"), "", "extra claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
}
compilationResult, err := validatePrefixClaimOrExpression(compiler, m.Username, fldPath.Child("username"), true)
if err != nil {
allErrs = append(allErrs, err...)
} else if compilationResult != nil && structuredAuthnFeatureEnabled {
state.usesEmailClaim = state.usesEmailClaim || usesEmailClaim(compilationResult.AST)
state.usesEmailVerifiedClaim = state.usesEmailVerifiedClaim || usesEmailVerifiedClaim(compilationResult.AST)
state.mapper.Username = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult})
}
compilationResult, err = validatePrefixClaimOrExpression(compiler, m.Groups, fldPath.Child("groups"), false)
if err != nil {
allErrs = append(allErrs, err...)
} else if compilationResult != nil && structuredAuthnFeatureEnabled {
state.mapper.Groups = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult})
}
switch {
case len(m.UID.Claim) > 0 && len(m.UID.Expression) > 0:
allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), "", "claim and expression can't both be set"))
case len(m.UID.Expression) > 0:
compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ClaimMappingExpression{
Expression: m.UID.Expression,
}, fldPath.Child("uid").Child("expression"))
if err != nil {
allErrs = append(allErrs, err)
} else if structuredAuthnFeatureEnabled && compilationResult != nil {
state.mapper.UID = authenticationcel.NewClaimsMapper([]authenticationcel.CompilationResult{*compilationResult})
}
}
var extraCompilationResults []authenticationcel.CompilationResult
seenExtraKeys := sets.NewString()
for i, mapping := range m.Extra {
fldPath := fldPath.Child("extra").Index(i)
// Key should be namespaced to the authenticator or authenticator/authorizer pair making use of them.
// For instance: "example.org/foo" instead of "foo".
// xref: https://github.com/kubernetes/kubernetes/blob/3825e206cb162a7ad7431a5bdf6a065ae8422cf7/staging/src/k8s.io/apiserver/pkg/authentication/user/user.go#L31-L41
// IsDomainPrefixedPath checks for non-empty key and that the key is prefixed with a domain name.
allErrs = append(allErrs, utilvalidation.IsDomainPrefixedPath(fldPath.Child("key"), mapping.Key)...)
if mapping.Key != strings.ToLower(mapping.Key) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), mapping.Key, "key must be lowercase"))
}
if isKubernetesDomainPrefix(mapping.Key) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), mapping.Key, "k8s.io, kubernetes.io and their subdomains are reserved for Kubernetes use"))
}
if seenExtraKeys.Has(mapping.Key) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("key"), mapping.Key))
continue
}
seenExtraKeys.Insert(mapping.Key)
if len(mapping.ValueExpression) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("valueExpression"), "valueExpression is required"))
continue
}
compilationResult, err := compileClaimsCELExpression(compiler, &authenticationcel.ExtraMappingExpression{
Key: mapping.Key,
Expression: mapping.ValueExpression,
}, fldPath.Child("valueExpression"))
if err != nil {
allErrs = append(allErrs, err)
continue
}
if compilationResult != nil {
extraCompilationResults = append(extraCompilationResults, *compilationResult)
}
}
if structuredAuthnFeatureEnabled && len(extraCompilationResults) > 0 {
state.mapper.Extra = authenticationcel.NewClaimsMapper(extraCompilationResults)
state.usesEmailVerifiedClaim = state.usesEmailVerifiedClaim || anyUsesEmailVerifiedClaim(extraCompilationResults)
}
if structuredAuthnFeatureEnabled && state.usesEmailClaim && !state.usesEmailVerifiedClaim {
allErrs = append(allErrs, field.Invalid(fldPath.Child("username", "expression"), m.Username.Expression,
"claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression"))
}
return allErrs
}
func isKubernetesDomainPrefix(key string) bool {
domainPrefix := getDomainPrefix(key)
if domainPrefix == "kubernetes.io" || strings.HasSuffix(domainPrefix, ".kubernetes.io") {
return true
}
if domainPrefix == "k8s.io" || strings.HasSuffix(domainPrefix, ".k8s.io") {
return true
}
return false
}
func getDomainPrefix(key string) string {
if parts := strings.SplitN(key, "/", 2); len(parts) == 2 {
return parts[0]
}
return ""
}
func usesEmailClaim(ast *celgo.Ast) bool {
return hasSelectExp(ast.Expr(), "claims", "email")
}
func anyUsesEmailVerifiedClaim(results []authenticationcel.CompilationResult) bool {
for _, result := range results {
if usesEmailVerifiedClaim(result.AST) {
return true
}
}
return false
}
func usesEmailVerifiedClaim(ast *celgo.Ast) bool {
return hasSelectExp(ast.Expr(), "claims", "email_verified")
}
func hasSelectExp(exp *exprpb.Expr, operand, field string) bool {
if exp == nil {
return false
}
switch e := exp.ExprKind.(type) {
case *exprpb.Expr_ConstExpr,
*exprpb.Expr_IdentExpr:
return false
case *exprpb.Expr_SelectExpr:
s := e.SelectExpr
if s == nil {
return false
}
if isIdentOperand(s.Operand, operand) && s.Field == field {
return true
}
return hasSelectExp(s.Operand, operand, field)
case *exprpb.Expr_CallExpr:
c := e.CallExpr
if c == nil {
return false
}
if c.Target == nil && c.Function == operators.OptSelect && len(c.Args) == 2 &&
isIdentOperand(c.Args[0], operand) && isConstField(c.Args[1], field) {
return true
}
for _, arg := range c.Args {
if hasSelectExp(arg, operand, field) {
return true
}
}
return hasSelectExp(c.Target, operand, field)
case *exprpb.Expr_ListExpr:
l := e.ListExpr
if l == nil {
return false
}
for _, element := range l.Elements {
if hasSelectExp(element, operand, field) {
return true
}
}
return false
case *exprpb.Expr_StructExpr:
s := e.StructExpr
if s == nil {
return false
}
for _, entry := range s.Entries {
if hasSelectExp(entry.GetMapKey(), operand, field) {
return true
}
if hasSelectExp(entry.Value, operand, field) {
return true
}
}
return false
case *exprpb.Expr_ComprehensionExpr:
c := e.ComprehensionExpr
if c == nil {
return false
}
return hasSelectExp(c.IterRange, operand, field) ||
hasSelectExp(c.AccuInit, operand, field) ||
hasSelectExp(c.LoopCondition, operand, field) ||
hasSelectExp(c.LoopStep, operand, field) ||
hasSelectExp(c.Result, operand, field)
default:
return false
}
}
func isIdentOperand(exp *exprpb.Expr, operand string) bool {
if len(operand) == 0 {
return false // sanity check against default values
}
id := exp.GetIdentExpr() // does not panic even if exp is nil
return id != nil && id.Name == operand
}
func isConstField(exp *exprpb.Expr, field string) bool {
if len(field) == 0 {
return false // sanity check against default values
}
c := exp.GetConstExpr() // does not panic even if exp is nil
return c != nil && c.GetStringValue() == field // does not panic even if c is not a string
}
func validatePrefixClaimOrExpression(compiler authenticationcel.Compiler, mapping api.PrefixedClaimOrExpression, fldPath *field.Path, claimOrExpressionRequired bool) (*authenticationcel.CompilationResult, field.ErrorList) {
var allErrs field.ErrorList
var compilationResult *authenticationcel.CompilationResult
switch {
case len(mapping.Expression) > 0 && len(mapping.Claim) > 0:
allErrs = append(allErrs, field.Invalid(fldPath, "", "claim and expression can't both be set"))
case len(mapping.Expression) == 0 && len(mapping.Claim) == 0 && claimOrExpressionRequired:
allErrs = append(allErrs, field.Required(fldPath, "claim or expression is required"))
case len(mapping.Expression) > 0:
var err *field.Error
if mapping.Prefix != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("prefix"), *mapping.Prefix, "prefix can't be set when expression is set"))
}
compilationResult, err = compileClaimsCELExpression(compiler, &authenticationcel.ClaimMappingExpression{
Expression: mapping.Expression,
}, fldPath.Child("expression"))
if err != nil {
allErrs = append(allErrs, err)
}
case len(mapping.Claim) > 0:
if mapping.Prefix == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("prefix"), "prefix is required when claim is set. It can be set to an empty string to disable prefixing"))
}
}
return compilationResult, allErrs
}
func validateUserValidationRules(compiler authenticationcel.Compiler, state *validationState, rules []api.UserValidationRule, fldPath *field.Path, structuredAuthnFeatureEnabled bool) field.ErrorList {
var allErrs field.ErrorList
var compilationResults []authenticationcel.CompilationResult
if len(rules) > 0 && !structuredAuthnFeatureEnabled {
allErrs = append(allErrs, field.Invalid(fldPath, "", "user validation rules are not supported when StructuredAuthenticationConfiguration feature gate is disabled"))
}
seenExpressions := sets.NewString()
for i, rule := range rules {
fldPath := fldPath.Index(i)
if len(rule.Expression) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("expression"), "expression is required"))
continue
}
if seenExpressions.Has(rule.Expression) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("expression"), rule.Expression))
continue
}
seenExpressions.Insert(rule.Expression)
compilationResult, err := compileUserCELExpression(compiler, &authenticationcel.UserValidationCondition{
Expression: rule.Expression,
Message: rule.Message,
}, fldPath.Child("expression"))
if err != nil {
allErrs = append(allErrs, err)
continue
}
if compilationResult != nil {
compilationResults = append(compilationResults, *compilationResult)
}
}
if structuredAuthnFeatureEnabled && len(compilationResults) > 0 {
state.mapper.UserValidationRules = authenticationcel.NewUserMapper(compilationResults)
}
return allErrs
}
func compileClaimsCELExpression(compiler authenticationcel.Compiler, expression authenticationcel.ExpressionAccessor, fldPath *field.Path) (*authenticationcel.CompilationResult, *field.Error) {
compilationResult, err := compiler.CompileClaimsExpression(expression)
if err != nil {
return nil, convertCELErrorToValidationError(fldPath, expression.GetExpression(), err)
}
return &compilationResult, nil
}
func compileUserCELExpression(compiler authenticationcel.Compiler, expression authenticationcel.ExpressionAccessor, fldPath *field.Path) (*authenticationcel.CompilationResult, *field.Error) {
compilationResult, err := compiler.CompileUserExpression(expression)
if err != nil {
return nil, convertCELErrorToValidationError(fldPath, expression.GetExpression(), err)
}
return &compilationResult, nil
}
// ValidateAuthorizationConfiguration validates a given AuthorizationConfiguration.
func ValidateAuthorizationConfiguration(compiler authorizationcel.Compiler, fldPath *field.Path, c *api.AuthorizationConfiguration, knownTypes sets.Set[string], repeatableTypes sets.Set[string]) field.ErrorList {
allErrs := field.ErrorList{}
if len(c.Authorizers) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("authorizers"), "at least one authorization mode must be defined"))
}
seenAuthorizerTypes := sets.NewString()
seenAuthorizerNames := sets.NewString()
for i, a := range c.Authorizers {
fldPath := fldPath.Child("authorizers").Index(i)
aType := string(a.Type)
if aType == "" {
allErrs = append(allErrs, field.Required(fldPath.Child("type"), ""))
continue
}
if !knownTypes.Has(aType) {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), aType, sets.List(knownTypes)))
continue
}
if seenAuthorizerTypes.Has(aType) && !repeatableTypes.Has(aType) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("type"), aType))
continue
}
seenAuthorizerTypes.Insert(aType)
if len(a.Name) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
} else if seenAuthorizerNames.Has(a.Name) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("name"), a.Name))
} else if errs := utilvalidation.IsDNS1123Subdomain(a.Name); len(errs) != 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), a.Name, fmt.Sprintf("authorizer name is invalid: %s", strings.Join(errs, ", "))))
}
seenAuthorizerNames.Insert(a.Name)
switch a.Type {
case api.TypeWebhook:
if a.Webhook == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("webhook"), "required when type=Webhook"))
continue
}
allErrs = append(allErrs, ValidateWebhookConfiguration(compiler, fldPath, a.Webhook)...)
default:
if a.Webhook != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("webhook"), "non-null", "may only be specified when type=Webhook"))
}
}
}
return allErrs
}
func ValidateWebhookConfiguration(compiler authorizationcel.Compiler, fldPath *field.Path, c *api.WebhookConfiguration) field.ErrorList {
allErrs := field.ErrorList{}
if c.Timeout.Duration == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("timeout"), ""))
} else if c.Timeout.Duration > 30*time.Second || c.Timeout.Duration < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("timeout"), c.Timeout.Duration.String(), "must be > 0s and <= 30s"))
}
if c.AuthorizedTTL.Duration == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("authorizedTTL"), ""))
} else if c.AuthorizedTTL.Duration < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("authorizedTTL"), c.AuthorizedTTL.Duration.String(), "must be > 0s"))
}
if c.UnauthorizedTTL.Duration == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("unauthorizedTTL"), ""))
} else if c.UnauthorizedTTL.Duration < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("unauthorizedTTL"), c.UnauthorizedTTL.Duration.String(), "must be > 0s"))
}
switch c.SubjectAccessReviewVersion {
case "":
allErrs = append(allErrs, field.Required(fldPath.Child("subjectAccessReviewVersion"), ""))
case "v1":
_ = &v1.SubjectAccessReview{}
case "v1beta1":
_ = &v1beta1.SubjectAccessReview{}
default:
allErrs = append(allErrs, field.NotSupported(fldPath.Child("subjectAccessReviewVersion"), c.SubjectAccessReviewVersion, []string{"v1", "v1beta1"}))
}
switch c.MatchConditionSubjectAccessReviewVersion {
case "":
if len(c.MatchConditions) > 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("matchConditionSubjectAccessReviewVersion"), "required if match conditions are specified"))
}
case "v1":
_ = &v1.SubjectAccessReview{}
default:
allErrs = append(allErrs, field.NotSupported(fldPath.Child("matchConditionSubjectAccessReviewVersion"), c.MatchConditionSubjectAccessReviewVersion, []string{"v1"}))
}
switch c.FailurePolicy {
case "":
allErrs = append(allErrs, field.Required(fldPath.Child("failurePolicy"), ""))
case api.FailurePolicyNoOpinion, api.FailurePolicyDeny:
default:
allErrs = append(allErrs, field.NotSupported(fldPath.Child("failurePolicy"), c.FailurePolicy, []string{"NoOpinion", "Deny"}))
}
switch c.ConnectionInfo.Type {
case "":
allErrs = append(allErrs, field.Required(fldPath.Child("connectionInfo", "type"), ""))
case api.AuthorizationWebhookConnectionInfoTypeInCluster:
if c.ConnectionInfo.KubeConfigFile != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("connectionInfo", "kubeConfigFile"), *c.ConnectionInfo.KubeConfigFile, "can only be set when type=KubeConfigFile"))
}
case api.AuthorizationWebhookConnectionInfoTypeKubeConfigFile:
if c.ConnectionInfo.KubeConfigFile == nil || *c.ConnectionInfo.KubeConfigFile == "" {
allErrs = append(allErrs, field.Required(fldPath.Child("connectionInfo", "kubeConfigFile"), ""))
} else if !filepath.IsAbs(*c.ConnectionInfo.KubeConfigFile) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("connectionInfo", "kubeConfigFile"), *c.ConnectionInfo.KubeConfigFile, "must be an absolute path"))
} else if info, err := os.Stat(*c.ConnectionInfo.KubeConfigFile); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("connectionInfo", "kubeConfigFile"), *c.ConnectionInfo.KubeConfigFile, fmt.Sprintf("error loading file: %v", err)))
} else if !info.Mode().IsRegular() {
allErrs = append(allErrs, field.Invalid(fldPath.Child("connectionInfo", "kubeConfigFile"), *c.ConnectionInfo.KubeConfigFile, "must be a regular file"))
}
default:
allErrs = append(allErrs, field.NotSupported(fldPath.Child("connectionInfo", "type"), c.ConnectionInfo, []string{api.AuthorizationWebhookConnectionInfoTypeInCluster, api.AuthorizationWebhookConnectionInfoTypeKubeConfigFile}))
}
_, errs := compileMatchConditions(compiler, c.MatchConditions, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
allErrs = append(allErrs, errs...)
return allErrs
}
// ValidateAndCompileMatchConditions validates a given webhook's matchConditions.
// This is exported for use in authz package.
func ValidateAndCompileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition) (*authorizationcel.CELMatcher, field.ErrorList) {
return compileMatchConditions(compiler, matchConditions, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
}
func compileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition, fldPath *field.Path, structuredAuthzFeatureEnabled bool) (*authorizationcel.CELMatcher, field.ErrorList) {
var allErrs field.ErrorList
// should fail when match conditions are used without feature enabled
if len(matchConditions) > 0 && !structuredAuthzFeatureEnabled {
allErrs = append(allErrs, field.Invalid(fldPath.Child("matchConditions"), "", "matchConditions are not supported when StructuredAuthorizationConfiguration feature gate is disabled"))
}
if len(matchConditions) > 64 {
allErrs = append(allErrs, field.TooMany(fldPath.Child("matchConditions"), len(matchConditions), 64))
return nil, allErrs
}
seenExpressions := sets.NewString()
var compilationResults []authorizationcel.CompilationResult
var usesFieldSelector, usesLabelSelector bool
for i, condition := range matchConditions {
fldPath := fldPath.Child("matchConditions").Index(i).Child("expression")
if len(strings.TrimSpace(condition.Expression)) == 0 {
allErrs = append(allErrs, field.Required(fldPath, ""))
continue
}
if seenExpressions.Has(condition.Expression) {
allErrs = append(allErrs, field.Duplicate(fldPath, condition.Expression))
continue
}
seenExpressions.Insert(condition.Expression)
compilationResult, err := compileMatchConditionsExpression(fldPath, compiler, condition.Expression)
if err != nil {
allErrs = append(allErrs, err)
continue
}
compilationResults = append(compilationResults, compilationResult)
usesFieldSelector = usesFieldSelector || compilationResult.UsesFieldSelector
usesLabelSelector = usesLabelSelector || compilationResult.UsesLabelSelector
}
if len(compilationResults) == 0 {
return nil, allErrs
}
return &authorizationcel.CELMatcher{
CompilationResults: compilationResults,
UsesFieldSelector: usesFieldSelector,
UsesLabelSelector: usesLabelSelector,
}, allErrs
}
func compileMatchConditionsExpression(fldPath *field.Path, compiler authorizationcel.Compiler, expression string) (authorizationcel.CompilationResult, *field.Error) {
authzExpression := &authorizationcel.SubjectAccessReviewMatchCondition{
Expression: expression,
}
compilationResult, err := compiler.CompileCELExpression(authzExpression)
if err != nil {
return compilationResult, convertCELErrorToValidationError(fldPath, authzExpression.GetExpression(), err)
}
return compilationResult, nil
}
func convertCELErrorToValidationError(fldPath *field.Path, expression string, err error) *field.Error {
var celErr *cel.Error
if errors.As(err, &celErr) {
switch celErr.Type {
case cel.ErrorTypeRequired:
return field.Required(fldPath, celErr.Detail)
case cel.ErrorTypeInvalid:
return field.Invalid(fldPath, expression, celErr.Detail)
default:
return field.InternalError(fldPath, celErr)
}
}
return field.InternalError(fldPath, fmt.Errorf("error is not cel error: %w", err))
}

View File

@ -1,451 +0,0 @@
/*
Copyright 2019 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 validation validates EncryptionConfiguration.
package validation
import (
"encoding/base64"
"fmt"
"net/url"
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/apis/apiserver"
)
const (
moreThanOneElementErr = "more than one provider specified in a single element, should split into different list elements"
keyLenErrFmt = "secret is not of the expected length, got %d, expected one of %v"
unsupportedSchemeErrFmt = "unsupported scheme %q for KMS provider, only unix is supported"
unsupportedKMSAPIVersionErrFmt = "unsupported apiVersion %s for KMS provider, only v1 and v2 are supported"
atLeastOneRequiredErrFmt = "at least one %s is required"
invalidURLErrFmt = "invalid endpoint for kms provider, error: %v"
mandatoryFieldErrFmt = "%s is a mandatory field for a %s"
base64EncodingErr = "secrets must be base64 encoded"
zeroOrNegativeErrFmt = "%s should be a positive value"
nonZeroErrFmt = "%s should be a positive value, or negative to disable"
encryptionConfigNilErr = "EncryptionConfiguration can't be nil"
invalidKMSConfigNameErrFmt = "invalid KMS provider name %s, must not contain ':'"
duplicateKMSConfigNameErrFmt = "duplicate KMS provider name %s, names must be unique"
eventsGroupErr = "'*.events.k8s.io' objects are stored using the 'events' API group in etcd. Use 'events' instead in the config file"
extensionsGroupErr = "'extensions' group has been removed and cannot be used for encryption"
starResourceErr = "use '*.' to encrypt all the resources from core API group or *.* to encrypt all resources"
overlapErr = "using overlapping resources such as 'secrets' and '*.' in the same resource list is not allowed as they will be masked"
nonRESTAPIResourceErr = "resources which do not have REST API/s cannot be encrypted"
resourceNameErr = "resource name should not contain capital letters"
resourceAcrossGroupErr = "encrypting the same resource across groups is not supported"
duplicateResourceErr = "the same resource cannot be specified multiple times"
)
var (
// See https://golang.org/pkg/crypto/aes/#NewCipher for details on supported key sizes for AES.
aesKeySizes = []int{16, 24, 32}
// See https://godoc.org/golang.org/x/crypto/nacl/secretbox#Open for details on the supported key sizes for Secretbox.
secretBoxKeySizes = []int{32}
)
// ValidateEncryptionConfiguration validates a v1.EncryptionConfiguration.
func ValidateEncryptionConfiguration(c *apiserver.EncryptionConfiguration, reload bool) field.ErrorList {
root := field.NewPath("resources")
allErrs := field.ErrorList{}
if c == nil {
allErrs = append(allErrs, field.Required(root, encryptionConfigNilErr))
return allErrs
}
if len(c.Resources) == 0 {
allErrs = append(allErrs, field.Required(root, fmt.Sprintf(atLeastOneRequiredErrFmt, root)))
return allErrs
}
// kmsProviderNames is used to track config names to ensure they are unique.
kmsProviderNames := sets.New[string]()
for i, conf := range c.Resources {
r := root.Index(i).Child("resources")
p := root.Index(i).Child("providers")
if len(conf.Resources) == 0 {
allErrs = append(allErrs, field.Required(r, fmt.Sprintf(atLeastOneRequiredErrFmt, r)))
}
allErrs = append(allErrs, validateResourceOverlap(conf.Resources, r)...)
allErrs = append(allErrs, validateResourceNames(conf.Resources, r)...)
if len(conf.Providers) == 0 {
allErrs = append(allErrs, field.Required(p, fmt.Sprintf(atLeastOneRequiredErrFmt, p)))
}
for j, provider := range conf.Providers {
path := p.Index(j)
allErrs = append(allErrs, validateSingleProvider(provider, path)...)
switch {
case provider.KMS != nil:
allErrs = append(allErrs, validateKMSConfiguration(provider.KMS, path.Child("kms"), kmsProviderNames, reload)...)
kmsProviderNames.Insert(provider.KMS.Name)
case provider.AESGCM != nil:
allErrs = append(allErrs, validateKeys(provider.AESGCM.Keys, path.Child("aesgcm").Child("keys"), aesKeySizes)...)
case provider.AESCBC != nil:
allErrs = append(allErrs, validateKeys(provider.AESCBC.Keys, path.Child("aescbc").Child("keys"), aesKeySizes)...)
case provider.Secretbox != nil:
allErrs = append(allErrs, validateKeys(provider.Secretbox.Keys, path.Child("secretbox").Child("keys"), secretBoxKeySizes)...)
}
}
}
return allErrs
}
var anyGroupAnyResource = schema.GroupResource{
Group: "*",
Resource: "*",
}
func validateResourceOverlap(resources []string, fieldPath *field.Path) field.ErrorList {
if len(resources) < 2 { // cannot have overlap with a single resource
return nil
}
var allErrs field.ErrorList
r := make([]schema.GroupResource, 0, len(resources))
for _, resource := range resources {
r = append(r, schema.ParseGroupResource(resource))
}
var hasOverlap, hasDuplicate bool
for i, r1 := range r {
for j, r2 := range r {
if i == j {
continue
}
if r1 == r2 && !hasDuplicate {
hasDuplicate = true
continue
}
if hasOverlap {
continue
}
if r1 == anyGroupAnyResource {
hasOverlap = true
continue
}
if r1.Group != r2.Group {
continue
}
if r1.Resource == "*" || r2.Resource == "*" {
hasOverlap = true
continue
}
}
}
if hasDuplicate {
allErrs = append(
allErrs,
field.Invalid(
fieldPath,
resources,
duplicateResourceErr,
),
)
}
if hasOverlap {
allErrs = append(
allErrs,
field.Invalid(
fieldPath,
resources,
overlapErr,
),
)
}
return allErrs
}
func validateResourceNames(resources []string, fieldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for j, res := range resources {
jj := fieldPath.Index(j)
// check if resource name has capital letters
if hasCapital(res) {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
resourceNameErr,
),
)
continue
}
// check if resource is '*'
if res == "*" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
starResourceErr,
),
)
continue
}
// check if resource is:
// 'apiserveripinfo' OR
// 'serviceipallocations' OR
// 'servicenodeportallocations' OR
if res == "apiserveripinfo" ||
res == "serviceipallocations" ||
res == "servicenodeportallocations" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
nonRESTAPIResourceErr,
),
)
continue
}
// check if group is 'events.k8s.io'
gr := schema.ParseGroupResource(res)
if gr.Group == "events.k8s.io" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
eventsGroupErr,
),
)
continue
}
// check if group is 'extensions'
if gr.Group == "extensions" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
extensionsGroupErr,
),
)
continue
}
// disallow resource.* as encrypting the same resource across groups does not make sense
if gr.Group == "*" && gr.Resource != "*" {
allErrs = append(
allErrs,
field.Invalid(
jj,
resources[j],
resourceAcrossGroupErr,
),
)
continue
}
}
return allErrs
}
func validateSingleProvider(provider apiserver.ProviderConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
found := 0
if provider.KMS != nil {
found++
}
if provider.AESGCM != nil {
found++
}
if provider.AESCBC != nil {
found++
}
if provider.Secretbox != nil {
found++
}
if provider.Identity != nil {
found++
}
if found == 0 {
return append(allErrs, field.Invalid(fieldPath, provider, "provider does not contain any of the expected providers: KMS, AESGCM, AESCBC, Secretbox, Identity"))
}
if found > 1 {
return append(allErrs, field.Invalid(fieldPath, provider, moreThanOneElementErr))
}
return allErrs
}
func validateKeys(keys []apiserver.Key, fieldPath *field.Path, expectedLen []int) field.ErrorList {
allErrs := field.ErrorList{}
if len(keys) == 0 {
allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, "keys")))
return allErrs
}
for i, key := range keys {
allErrs = append(allErrs, validateKey(key, fieldPath.Index(i), expectedLen)...)
}
return allErrs
}
func validateKey(key apiserver.Key, fieldPath *field.Path, expectedLen []int) field.ErrorList {
allErrs := field.ErrorList{}
if key.Name == "" {
allErrs = append(allErrs, field.Required(fieldPath.Child("name"), fmt.Sprintf(mandatoryFieldErrFmt, "name", "key")))
}
if key.Secret == "" {
allErrs = append(allErrs, field.Required(fieldPath.Child("secret"), fmt.Sprintf(mandatoryFieldErrFmt, "secret", "key")))
return allErrs
}
secret, err := base64.StdEncoding.DecodeString(key.Secret)
if err != nil {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("secret"), "REDACTED", base64EncodingErr))
return allErrs
}
lenMatched := false
for _, l := range expectedLen {
if len(secret) == l {
lenMatched = true
break
}
}
if !lenMatched {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("secret"), "REDACTED", fmt.Sprintf(keyLenErrFmt, len(secret), expectedLen)))
}
return allErrs
}
func validateKMSConfiguration(c *apiserver.KMSConfiguration, fieldPath *field.Path, kmsProviderNames sets.Set[string], reload bool) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, validateKMSConfigName(c, fieldPath.Child("name"), kmsProviderNames, reload)...)
allErrs = append(allErrs, validateKMSTimeout(c, fieldPath.Child("timeout"))...)
allErrs = append(allErrs, validateKMSEndpoint(c, fieldPath.Child("endpoint"))...)
allErrs = append(allErrs, validateKMSCacheSize(c, fieldPath.Child("cachesize"))...)
allErrs = append(allErrs, validateKMSAPIVersion(c, fieldPath.Child("apiVersion"))...)
return allErrs
}
func validateKMSCacheSize(c *apiserver.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
// In defaulting, we set the cache size to the default value only when API version is v1.
// So, for v2 API version, we expect the cache size field to be nil.
if c.APIVersion != "v1" && c.CacheSize != nil {
allErrs = append(allErrs, field.Invalid(fieldPath, *c.CacheSize, "cachesize is not supported in v2"))
}
if c.APIVersion == "v1" && *c.CacheSize == 0 {
allErrs = append(allErrs, field.Invalid(fieldPath, *c.CacheSize, fmt.Sprintf(nonZeroErrFmt, "cachesize")))
}
return allErrs
}
func validateKMSTimeout(c *apiserver.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if c.Timeout.Duration <= 0 {
allErrs = append(allErrs, field.Invalid(fieldPath, c.Timeout, fmt.Sprintf(zeroOrNegativeErrFmt, "timeout")))
}
return allErrs
}
func validateKMSEndpoint(c *apiserver.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(c.Endpoint) == 0 {
return append(allErrs, field.Invalid(fieldPath, "", fmt.Sprintf(mandatoryFieldErrFmt, "endpoint", "kms")))
}
u, err := url.Parse(c.Endpoint)
if err != nil {
return append(allErrs, field.Invalid(fieldPath, c.Endpoint, fmt.Sprintf(invalidURLErrFmt, err)))
}
if u.Scheme != "unix" {
return append(allErrs, field.Invalid(fieldPath, c.Endpoint, fmt.Sprintf(unsupportedSchemeErrFmt, u.Scheme)))
}
return allErrs
}
func validateKMSAPIVersion(c *apiserver.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if c.APIVersion != "v1" && c.APIVersion != "v2" {
allErrs = append(allErrs, field.Invalid(fieldPath, c.APIVersion, fmt.Sprintf(unsupportedKMSAPIVersionErrFmt, "apiVersion")))
}
return allErrs
}
func validateKMSConfigName(c *apiserver.KMSConfiguration, fieldPath *field.Path, kmsProviderNames sets.Set[string], reload bool) field.ErrorList {
allErrs := field.ErrorList{}
if c.Name == "" {
allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf(mandatoryFieldErrFmt, "name", "provider")))
}
// kms v2 providers are not allowed to have a ":" in their name
if c.APIVersion != "v1" && strings.Contains(c.Name, ":") {
allErrs = append(allErrs, field.Invalid(fieldPath, c.Name, fmt.Sprintf(invalidKMSConfigNameErrFmt, c.Name)))
}
// kms v2 providers name must always be unique across all kms providers (v1 and v2)
// kms v1 provider names must be unique across all kms providers (v1 and v2) when hot reloading of encryption configuration is enabled (reload=true)
if reload || c.APIVersion != "v1" {
if kmsProviderNames.Has(c.Name) {
allErrs = append(allErrs, field.Invalid(fieldPath, c.Name, fmt.Sprintf(duplicateKMSConfigNameErrFmt, c.Name)))
}
}
return allErrs
}
func hasCapital(input string) bool {
return strings.ToLower(input) != input
}

View File

@ -1,33 +0,0 @@
/*
Copyright 2017 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 install installs the experimental API group, making it available as
// an option to all of the API encoding/decoding machinery.
package install
import (
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/apis/audit/v1"
)
// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
utilruntime.Must(audit.AddToScheme(scheme))
utilruntime.Must(v1.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion))
}

View File

@ -1,133 +0,0 @@
/*
Copyright 2017 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 validation
import (
"strings"
"k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/apis/audit"
)
// ValidatePolicy validates the audit policy
func ValidatePolicy(policy *audit.Policy) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateOmitStages(policy.OmitStages, field.NewPath("omitStages"))...)
rulePath := field.NewPath("rules")
for i, rule := range policy.Rules {
allErrs = append(allErrs, validatePolicyRule(rule, rulePath.Index(i))...)
}
return allErrs
}
func validatePolicyRule(rule audit.PolicyRule, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateLevel(rule.Level, fldPath.Child("level"))...)
allErrs = append(allErrs, validateNonResourceURLs(rule.NonResourceURLs, fldPath.Child("nonResourceURLs"))...)
allErrs = append(allErrs, validateResources(rule.Resources, fldPath.Child("resources"))...)
allErrs = append(allErrs, validateOmitStages(rule.OmitStages, fldPath.Child("omitStages"))...)
if len(rule.NonResourceURLs) > 0 {
if len(rule.Resources) > 0 || len(rule.Namespaces) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("nonResourceURLs"), rule.NonResourceURLs, "rules cannot apply to both regular resources and non-resource URLs"))
}
}
return allErrs
}
var validLevels = []string{
string(audit.LevelNone),
string(audit.LevelMetadata),
string(audit.LevelRequest),
string(audit.LevelRequestResponse),
}
var validOmitStages = []string{
string(audit.StageRequestReceived),
string(audit.StageResponseStarted),
string(audit.StageResponseComplete),
string(audit.StagePanic),
}
func validateLevel(level audit.Level, fldPath *field.Path) field.ErrorList {
switch level {
case audit.LevelNone, audit.LevelMetadata, audit.LevelRequest, audit.LevelRequestResponse:
return nil
case "":
return field.ErrorList{field.Required(fldPath, "")}
default:
return field.ErrorList{field.NotSupported(fldPath, level, validLevels)}
}
}
func validateNonResourceURLs(urls []string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for i, url := range urls {
if url == "*" {
continue
}
if !strings.HasPrefix(url, "/") {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), url, "non-resource URL rules must begin with a '/' character"))
}
if url != "" && strings.ContainsRune(url[:len(url)-1], '*') {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), url, "non-resource URL wildcards '*' must be the final character of the rule"))
}
}
return allErrs
}
func validateResources(groupResources []audit.GroupResources, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for _, groupResource := range groupResources {
// The empty string represents the core API group.
if len(groupResource.Group) != 0 {
// Group names must be lower case and be valid DNS subdomains.
// reference: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md
// an error is returned for group name like rbac.authorization.k8s.io/v1beta1
// rbac.authorization.k8s.io is the valid one
if msgs := validation.NameIsDNSSubdomain(groupResource.Group, false); len(msgs) != 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), groupResource.Group, strings.Join(msgs, ",")))
}
}
if len(groupResource.ResourceNames) > 0 && len(groupResource.Resources) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceNames"), groupResource.ResourceNames, "using resourceNames requires at least one resource"))
}
}
return allErrs
}
func validateOmitStages(omitStages []audit.Stage, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for i, stage := range omitStages {
valid := false
for _, validOmitStage := range validOmitStages {
if string(stage) == validOmitStage {
valid = true
break
}
}
if !valid {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), string(stage), "allowed stages are "+strings.Join(validOmitStages, ",")))
}
}
return allErrs
}

View File

@ -1,577 +0,0 @@
/*
Copyright 2019 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 bootstrap
import (
coordinationv1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
flowcontrol "k8s.io/api/flowcontrol/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/utils/ptr"
)
// The objects that define an apiserver's initial behavior. The
// registered defaulting procedures make no changes to these
// particular objects (this is verified in the unit tests of the
// internalbootstrap package; it can not be verified in this package
// because that would require importing k8s.io/kubernetes).
var (
MandatoryPriorityLevelConfigurations = []*flowcontrol.PriorityLevelConfiguration{
MandatoryPriorityLevelConfigurationCatchAll,
MandatoryPriorityLevelConfigurationExempt,
}
MandatoryFlowSchemas = []*flowcontrol.FlowSchema{
MandatoryFlowSchemaExempt,
MandatoryFlowSchemaCatchAll,
}
)
// The objects that define the current suggested additional configuration
var (
SuggestedPriorityLevelConfigurations = []*flowcontrol.PriorityLevelConfiguration{
// "system" priority-level is for the system components that affects self-maintenance of the
// cluster and the availability of those running pods in the cluster, including kubelet and
// kube-proxy.
SuggestedPriorityLevelConfigurationSystem,
// "node-high" priority-level is for the node health reporting. It is separated from "system"
// to make sure that nodes are able to report their health even if kube-apiserver is not capable of
// handling load caused by pod startup (fetching secrets, events etc).
// NOTE: In large clusters 50% - 90% of all API calls use this priority-level.
SuggestedPriorityLevelConfigurationNodeHigh,
// "leader-election" is dedicated for controllers' leader-election, which majorly affects the
// availability of any controller runs in the cluster.
SuggestedPriorityLevelConfigurationLeaderElection,
// "workload-high" is used by those workloads with higher priority but their failure won't directly
// impact the existing running pods in the cluster, which includes kube-scheduler, and those well-known
// built-in workloads such as "deployments", "replicasets" and other low-level custom workload which
// is important for the cluster.
SuggestedPriorityLevelConfigurationWorkloadHigh,
// "workload-low" is used by those workloads with lower priority which availability only has a
// minor impact on the cluster.
SuggestedPriorityLevelConfigurationWorkloadLow,
// "global-default" serves the rest traffic not handled by the other suggested flow-schemas above.
SuggestedPriorityLevelConfigurationGlobalDefault,
}
SuggestedFlowSchemas = []*flowcontrol.FlowSchema{
SuggestedFlowSchemaSystemNodes, // references "system" priority-level
SuggestedFlowSchemaSystemNodeHigh, // references "node-high" priority-level
SuggestedFlowSchemaProbes, // references "exempt" priority-level
SuggestedFlowSchemaSystemLeaderElection, // references "leader-election" priority-level
SuggestedFlowSchemaWorkloadLeaderElection, // references "leader-election" priority-level
SuggestedFlowSchemaEndpointsController, // references "workload-high" priority-level
SuggestedFlowSchemaKubeControllerManager, // references "workload-high" priority-level
SuggestedFlowSchemaKubeScheduler, // references "workload-high" priority-level
SuggestedFlowSchemaKubeSystemServiceAccounts, // references "workload-high" priority-level
SuggestedFlowSchemaServiceAccounts, // references "workload-low" priority-level
SuggestedFlowSchemaGlobalDefault, // references "global-default" priority-level
}
)
// Mandatory PriorityLevelConfiguration objects
var (
MandatoryPriorityLevelConfigurationExempt = newPriorityLevelConfiguration(
flowcontrol.PriorityLevelConfigurationNameExempt,
flowcontrol.PriorityLevelConfigurationSpec{
Type: flowcontrol.PriorityLevelEnablementExempt,
Exempt: &flowcontrol.ExemptPriorityLevelConfiguration{
NominalConcurrencyShares: ptr.To(int32(0)),
LendablePercent: ptr.To(int32(0)),
},
},
)
MandatoryPriorityLevelConfigurationCatchAll = newPriorityLevelConfiguration(
flowcontrol.PriorityLevelConfigurationNameCatchAll,
flowcontrol.PriorityLevelConfigurationSpec{
Type: flowcontrol.PriorityLevelEnablementLimited,
Limited: &flowcontrol.LimitedPriorityLevelConfiguration{
NominalConcurrencyShares: ptr.To(int32(5)),
LendablePercent: ptr.To(int32(0)),
LimitResponse: flowcontrol.LimitResponse{
Type: flowcontrol.LimitResponseTypeReject,
},
},
})
)
// Mandatory FlowSchema objects
var (
// "exempt" priority-level is used for preventing priority inversion and ensuring that sysadmin
// requests are always possible.
MandatoryFlowSchemaExempt = newFlowSchema(
"exempt",
flowcontrol.PriorityLevelConfigurationNameExempt,
1, // matchingPrecedence
"", // distinguisherMethodType
flowcontrol.PolicyRulesWithSubjects{
Subjects: groups(user.SystemPrivilegedGroup),
ResourceRules: []flowcontrol.ResourcePolicyRule{
resourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.APIGroupAll},
[]string{flowcontrol.ResourceAll},
[]string{flowcontrol.NamespaceEvery},
true,
),
},
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.NonResourceAll},
),
},
},
)
// "catch-all" priority-level only gets a minimal positive share of concurrency and won't be reaching
// ideally unless you intentionally deleted the suggested "global-default".
MandatoryFlowSchemaCatchAll = newFlowSchema(
flowcontrol.FlowSchemaNameCatchAll,
flowcontrol.PriorityLevelConfigurationNameCatchAll,
10000, // matchingPrecedence
flowcontrol.FlowDistinguisherMethodByUserType, // distinguisherMethodType
flowcontrol.PolicyRulesWithSubjects{
Subjects: groups(user.AllUnauthenticated, user.AllAuthenticated),
ResourceRules: []flowcontrol.ResourcePolicyRule{
resourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.APIGroupAll},
[]string{flowcontrol.ResourceAll},
[]string{flowcontrol.NamespaceEvery},
true,
),
},
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.NonResourceAll},
),
},
},
)
)
// Suggested PriorityLevelConfiguration objects
var (
// system priority-level
SuggestedPriorityLevelConfigurationSystem = newPriorityLevelConfiguration(
"system",
flowcontrol.PriorityLevelConfigurationSpec{
Type: flowcontrol.PriorityLevelEnablementLimited,
Limited: &flowcontrol.LimitedPriorityLevelConfiguration{
NominalConcurrencyShares: ptr.To(int32(30)),
LendablePercent: ptr.To(int32(33)),
LimitResponse: flowcontrol.LimitResponse{
Type: flowcontrol.LimitResponseTypeQueue,
Queuing: &flowcontrol.QueuingConfiguration{
Queues: 64,
HandSize: 6,
QueueLengthLimit: 50,
},
},
},
})
SuggestedPriorityLevelConfigurationNodeHigh = newPriorityLevelConfiguration(
"node-high",
flowcontrol.PriorityLevelConfigurationSpec{
Type: flowcontrol.PriorityLevelEnablementLimited,
Limited: &flowcontrol.LimitedPriorityLevelConfiguration{
NominalConcurrencyShares: ptr.To(int32(40)),
LendablePercent: ptr.To(int32(25)),
LimitResponse: flowcontrol.LimitResponse{
Type: flowcontrol.LimitResponseTypeQueue,
Queuing: &flowcontrol.QueuingConfiguration{
Queues: 64,
HandSize: 6,
QueueLengthLimit: 50,
},
},
},
})
// leader-election priority-level
SuggestedPriorityLevelConfigurationLeaderElection = newPriorityLevelConfiguration(
"leader-election",
flowcontrol.PriorityLevelConfigurationSpec{
Type: flowcontrol.PriorityLevelEnablementLimited,
Limited: &flowcontrol.LimitedPriorityLevelConfiguration{
NominalConcurrencyShares: ptr.To(int32(10)),
LendablePercent: ptr.To(int32(0)),
LimitResponse: flowcontrol.LimitResponse{
Type: flowcontrol.LimitResponseTypeQueue,
Queuing: &flowcontrol.QueuingConfiguration{
Queues: 16,
HandSize: 4,
QueueLengthLimit: 50,
},
},
},
})
// workload-high priority-level
SuggestedPriorityLevelConfigurationWorkloadHigh = newPriorityLevelConfiguration(
"workload-high",
flowcontrol.PriorityLevelConfigurationSpec{
Type: flowcontrol.PriorityLevelEnablementLimited,
Limited: &flowcontrol.LimitedPriorityLevelConfiguration{
NominalConcurrencyShares: ptr.To(int32(40)),
LendablePercent: ptr.To(int32(50)),
LimitResponse: flowcontrol.LimitResponse{
Type: flowcontrol.LimitResponseTypeQueue,
Queuing: &flowcontrol.QueuingConfiguration{
Queues: 128,
HandSize: 6,
QueueLengthLimit: 50,
},
},
},
})
// workload-low priority-level
SuggestedPriorityLevelConfigurationWorkloadLow = newPriorityLevelConfiguration(
"workload-low",
flowcontrol.PriorityLevelConfigurationSpec{
Type: flowcontrol.PriorityLevelEnablementLimited,
Limited: &flowcontrol.LimitedPriorityLevelConfiguration{
NominalConcurrencyShares: ptr.To(int32(100)),
LendablePercent: ptr.To(int32(90)),
LimitResponse: flowcontrol.LimitResponse{
Type: flowcontrol.LimitResponseTypeQueue,
Queuing: &flowcontrol.QueuingConfiguration{
Queues: 128,
HandSize: 6,
QueueLengthLimit: 50,
},
},
},
})
// global-default priority-level
SuggestedPriorityLevelConfigurationGlobalDefault = newPriorityLevelConfiguration(
"global-default",
flowcontrol.PriorityLevelConfigurationSpec{
Type: flowcontrol.PriorityLevelEnablementLimited,
Limited: &flowcontrol.LimitedPriorityLevelConfiguration{
NominalConcurrencyShares: ptr.To(int32(20)),
LendablePercent: ptr.To(int32(50)),
LimitResponse: flowcontrol.LimitResponse{
Type: flowcontrol.LimitResponseTypeQueue,
Queuing: &flowcontrol.QueuingConfiguration{
Queues: 128,
HandSize: 6,
QueueLengthLimit: 50,
},
},
},
})
)
// Suggested FlowSchema objects.
// Ordered by matching precedence, so that their interactions are easier
// to follow while reading this source.
var (
// the following flow schema exempts probes
SuggestedFlowSchemaProbes = newFlowSchema(
"probes", "exempt", 2,
"", // distinguisherMethodType
flowcontrol.PolicyRulesWithSubjects{
Subjects: groups(user.AllUnauthenticated, user.AllAuthenticated),
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{"get"},
[]string{"/healthz", "/readyz", "/livez"}),
},
},
)
SuggestedFlowSchemaSystemLeaderElection = newFlowSchema(
"system-leader-election", "leader-election", 100,
flowcontrol.FlowDistinguisherMethodByUserType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: append(
users(user.KubeControllerManager, user.KubeScheduler),
kubeSystemServiceAccount(flowcontrol.NameAll)...),
ResourceRules: []flowcontrol.ResourcePolicyRule{
resourceRule(
[]string{"get", "create", "update"},
[]string{coordinationv1.GroupName},
[]string{"leases"},
[]string{flowcontrol.NamespaceEvery},
false),
},
},
)
// We add an explicit rule for endpoint-controller with high precedence
// to ensure that those calls won't get caught by the following
// <workload-leader-election> flow-schema.
//
// TODO(#80289): Get rid of this rule once we get rid of support for
// using endpoints and configmaps objects for leader election.
SuggestedFlowSchemaEndpointsController = newFlowSchema(
"endpoint-controller", "workload-high", 150,
flowcontrol.FlowDistinguisherMethodByUserType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: append(
users(user.KubeControllerManager),
kubeSystemServiceAccount("endpoint-controller", "endpointslicemirroring-controller")...),
ResourceRules: []flowcontrol.ResourcePolicyRule{
resourceRule(
[]string{"get", "create", "update"},
[]string{corev1.GroupName},
[]string{"endpoints"},
[]string{flowcontrol.NamespaceEvery},
false),
},
},
)
// TODO(#80289): Get rid of this rule once we get rid of support for
// using endpoints and configmaps objects for leader election.
SuggestedFlowSchemaWorkloadLeaderElection = newFlowSchema(
"workload-leader-election", "leader-election", 200,
flowcontrol.FlowDistinguisherMethodByUserType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: kubeSystemServiceAccount(flowcontrol.NameAll),
ResourceRules: []flowcontrol.ResourcePolicyRule{
resourceRule(
[]string{"get", "create", "update"},
[]string{corev1.GroupName},
[]string{"endpoints", "configmaps"},
[]string{flowcontrol.NamespaceEvery},
false),
resourceRule(
[]string{"get", "create", "update"},
[]string{coordinationv1.GroupName},
[]string{"leases"},
[]string{flowcontrol.NamespaceEvery},
false),
},
},
)
SuggestedFlowSchemaSystemNodeHigh = newFlowSchema(
"system-node-high", "node-high", 400,
flowcontrol.FlowDistinguisherMethodByUserType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: groups(user.NodesGroup), // the nodes group
ResourceRules: []flowcontrol.ResourcePolicyRule{
resourceRule(
[]string{flowcontrol.VerbAll},
[]string{corev1.GroupName},
[]string{"nodes", "nodes/status"},
[]string{flowcontrol.NamespaceEvery},
true),
resourceRule(
[]string{flowcontrol.VerbAll},
[]string{coordinationv1.GroupName},
[]string{"leases"},
[]string{flowcontrol.NamespaceEvery},
false),
},
},
)
SuggestedFlowSchemaSystemNodes = newFlowSchema(
"system-nodes", "system", 500,
flowcontrol.FlowDistinguisherMethodByUserType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: groups(user.NodesGroup), // the nodes group
ResourceRules: []flowcontrol.ResourcePolicyRule{resourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.APIGroupAll},
[]string{flowcontrol.ResourceAll},
[]string{flowcontrol.NamespaceEvery},
true)},
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.NonResourceAll}),
},
},
)
SuggestedFlowSchemaKubeControllerManager = newFlowSchema(
"kube-controller-manager", "workload-high", 800,
flowcontrol.FlowDistinguisherMethodByNamespaceType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: users(user.KubeControllerManager),
ResourceRules: []flowcontrol.ResourcePolicyRule{resourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.APIGroupAll},
[]string{flowcontrol.ResourceAll},
[]string{flowcontrol.NamespaceEvery},
true)},
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.NonResourceAll}),
},
},
)
SuggestedFlowSchemaKubeScheduler = newFlowSchema(
"kube-scheduler", "workload-high", 800,
flowcontrol.FlowDistinguisherMethodByNamespaceType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: users(user.KubeScheduler),
ResourceRules: []flowcontrol.ResourcePolicyRule{resourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.APIGroupAll},
[]string{flowcontrol.ResourceAll},
[]string{flowcontrol.NamespaceEvery},
true)},
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.NonResourceAll}),
},
},
)
SuggestedFlowSchemaKubeSystemServiceAccounts = newFlowSchema(
"kube-system-service-accounts", "workload-high", 900,
flowcontrol.FlowDistinguisherMethodByNamespaceType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: kubeSystemServiceAccount(flowcontrol.NameAll),
ResourceRules: []flowcontrol.ResourcePolicyRule{resourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.APIGroupAll},
[]string{flowcontrol.ResourceAll},
[]string{flowcontrol.NamespaceEvery},
true)},
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.NonResourceAll}),
},
},
)
SuggestedFlowSchemaServiceAccounts = newFlowSchema(
"service-accounts", "workload-low", 9000,
flowcontrol.FlowDistinguisherMethodByUserType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: groups(serviceaccount.AllServiceAccountsGroup),
ResourceRules: []flowcontrol.ResourcePolicyRule{resourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.APIGroupAll},
[]string{flowcontrol.ResourceAll},
[]string{flowcontrol.NamespaceEvery},
true)},
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.NonResourceAll}),
},
},
)
SuggestedFlowSchemaGlobalDefault = newFlowSchema(
"global-default", "global-default", 9900,
flowcontrol.FlowDistinguisherMethodByUserType,
flowcontrol.PolicyRulesWithSubjects{
Subjects: groups(user.AllUnauthenticated, user.AllAuthenticated),
ResourceRules: []flowcontrol.ResourcePolicyRule{resourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.APIGroupAll},
[]string{flowcontrol.ResourceAll},
[]string{flowcontrol.NamespaceEvery},
true)},
NonResourceRules: []flowcontrol.NonResourcePolicyRule{
nonResourceRule(
[]string{flowcontrol.VerbAll},
[]string{flowcontrol.NonResourceAll}),
},
},
)
)
func newPriorityLevelConfiguration(name string, spec flowcontrol.PriorityLevelConfigurationSpec) *flowcontrol.PriorityLevelConfiguration {
return &flowcontrol.PriorityLevelConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: map[string]string{
flowcontrol.AutoUpdateAnnotationKey: "true",
},
},
Spec: spec,
}
}
func newFlowSchema(name, plName string, matchingPrecedence int32, dmType flowcontrol.FlowDistinguisherMethodType, rules ...flowcontrol.PolicyRulesWithSubjects) *flowcontrol.FlowSchema {
var dm *flowcontrol.FlowDistinguisherMethod
if dmType != "" {
dm = &flowcontrol.FlowDistinguisherMethod{Type: dmType}
}
return &flowcontrol.FlowSchema{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: map[string]string{
flowcontrol.AutoUpdateAnnotationKey: "true",
},
},
Spec: flowcontrol.FlowSchemaSpec{
PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{
Name: plName,
},
MatchingPrecedence: matchingPrecedence,
DistinguisherMethod: dm,
Rules: rules},
}
}
func groups(names ...string) []flowcontrol.Subject {
ans := make([]flowcontrol.Subject, len(names))
for idx, name := range names {
ans[idx] = flowcontrol.Subject{
Kind: flowcontrol.SubjectKindGroup,
Group: &flowcontrol.GroupSubject{
Name: name,
},
}
}
return ans
}
func users(names ...string) []flowcontrol.Subject {
ans := make([]flowcontrol.Subject, len(names))
for idx, name := range names {
ans[idx] = flowcontrol.Subject{
Kind: flowcontrol.SubjectKindUser,
User: &flowcontrol.UserSubject{
Name: name,
},
}
}
return ans
}
func kubeSystemServiceAccount(names ...string) []flowcontrol.Subject {
subjects := []flowcontrol.Subject{}
for _, name := range names {
subjects = append(subjects, flowcontrol.Subject{
Kind: flowcontrol.SubjectKindServiceAccount,
ServiceAccount: &flowcontrol.ServiceAccountSubject{
Name: name,
Namespace: metav1.NamespaceSystem,
},
})
}
return subjects
}
func resourceRule(verbs []string, groups []string, resources []string, namespaces []string, clusterScoped bool) flowcontrol.ResourcePolicyRule {
return flowcontrol.ResourcePolicyRule{
Verbs: verbs,
APIGroups: groups,
Resources: resources,
Namespaces: namespaces,
ClusterScope: clusterScoped,
}
}
func nonResourceRule(verbs []string, nonResourceURLs []string) flowcontrol.NonResourcePolicyRule {
return flowcontrol.NonResourcePolicyRule{Verbs: verbs, NonResourceURLs: nonResourceURLs}
}

View File

@ -1,239 +0,0 @@
/*
Copyright 2017 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 policy
import (
"strings"
"k8s.io/apiserver/pkg/apis/audit"
auditinternal "k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
const (
// DefaultAuditLevel is the default level to audit at, if no policy rules are matched.
DefaultAuditLevel = audit.LevelNone
)
// NewPolicyRuleEvaluator creates a new policy rule evaluator.
func NewPolicyRuleEvaluator(policy *audit.Policy) auditinternal.PolicyRuleEvaluator {
for i, rule := range policy.Rules {
policy.Rules[i].OmitStages = unionStages(policy.OmitStages, rule.OmitStages)
}
return &policyRuleEvaluator{*policy}
}
func unionStages(stageLists ...[]audit.Stage) []audit.Stage {
m := make(map[audit.Stage]bool)
for _, sl := range stageLists {
for _, s := range sl {
m[s] = true
}
}
result := make([]audit.Stage, 0, len(m))
for key := range m {
result = append(result, key)
}
return result
}
// NewFakePolicyRuleEvaluator creates a fake policy rule evaluator that returns
// a constant level for all requests (for testing).
func NewFakePolicyRuleEvaluator(level audit.Level, stage []audit.Stage) auditinternal.PolicyRuleEvaluator {
return &fakePolicyRuleEvaluator{level, stage}
}
type policyRuleEvaluator struct {
audit.Policy
}
func (p *policyRuleEvaluator) EvaluatePolicyRule(attrs authorizer.Attributes) auditinternal.RequestAuditConfig {
for _, rule := range p.Rules {
if ruleMatches(&rule, attrs) {
return auditinternal.RequestAuditConfig{
Level: rule.Level,
OmitStages: rule.OmitStages,
OmitManagedFields: isOmitManagedFields(&rule, p.OmitManagedFields),
}
}
}
return auditinternal.RequestAuditConfig{
Level: DefaultAuditLevel,
OmitStages: p.OmitStages,
OmitManagedFields: p.OmitManagedFields,
}
}
// isOmitManagedFields returns whether to omit managed fields from the request
// and response bodies from being written to the API audit log.
// If a user specifies OmitManagedFields inside a policy rule, that overrides
// the global policy default in Policy.OmitManagedFields.
func isOmitManagedFields(policyRule *audit.PolicyRule, policyDefault bool) bool {
if policyRule.OmitManagedFields == nil {
return policyDefault
}
return *policyRule.OmitManagedFields
}
// Check whether the rule matches the request attrs.
func ruleMatches(r *audit.PolicyRule, attrs authorizer.Attributes) bool {
user := attrs.GetUser()
if len(r.Users) > 0 {
if user == nil || !hasString(r.Users, user.GetName()) {
return false
}
}
if len(r.UserGroups) > 0 {
if user == nil {
return false
}
matched := false
for _, group := range user.GetGroups() {
if hasString(r.UserGroups, group) {
matched = true
break
}
}
if !matched {
return false
}
}
if len(r.Verbs) > 0 {
if !hasString(r.Verbs, attrs.GetVerb()) {
return false
}
}
if len(r.Namespaces) > 0 || len(r.Resources) > 0 {
return ruleMatchesResource(r, attrs)
}
if len(r.NonResourceURLs) > 0 {
return ruleMatchesNonResource(r, attrs)
}
return true
}
// Check whether the rule's non-resource URLs match the request attrs.
func ruleMatchesNonResource(r *audit.PolicyRule, attrs authorizer.Attributes) bool {
if attrs.IsResourceRequest() {
return false
}
path := attrs.GetPath()
for _, spec := range r.NonResourceURLs {
if pathMatches(path, spec) {
return true
}
}
return false
}
// Check whether the path matches the path specification.
func pathMatches(path, spec string) bool {
// Allow wildcard match
if spec == "*" {
return true
}
// Allow exact match
if spec == path {
return true
}
// Allow a trailing * subpath match
if strings.HasSuffix(spec, "*") && strings.HasPrefix(path, strings.TrimRight(spec, "*")) {
return true
}
return false
}
// Check whether the rule's resource fields match the request attrs.
func ruleMatchesResource(r *audit.PolicyRule, attrs authorizer.Attributes) bool {
if !attrs.IsResourceRequest() {
return false
}
if len(r.Namespaces) > 0 {
if !hasString(r.Namespaces, attrs.GetNamespace()) { // Non-namespaced resources use the empty string.
return false
}
}
if len(r.Resources) == 0 {
return true
}
apiGroup := attrs.GetAPIGroup()
resource := attrs.GetResource()
subresource := attrs.GetSubresource()
combinedResource := resource
// If subresource, the resource in the policy must match "(resource)/(subresource)"
if subresource != "" {
combinedResource = resource + "/" + subresource
}
name := attrs.GetName()
for _, gr := range r.Resources {
if gr.Group == apiGroup {
if len(gr.Resources) == 0 {
return true
}
for _, res := range gr.Resources {
if len(gr.ResourceNames) == 0 || hasString(gr.ResourceNames, name) {
// match "*"
if res == combinedResource || res == "*" {
return true
}
// match "*/subresource"
if len(subresource) > 0 && strings.HasPrefix(res, "*/") && subresource == strings.TrimPrefix(res, "*/") {
return true
}
// match "resource/*"
if strings.HasSuffix(res, "/*") && resource == strings.TrimSuffix(res, "/*") {
return true
}
}
}
}
}
return false
}
// Utility function to check whether a string slice contains a string.
func hasString(slice []string, value string) bool {
for _, s := range slice {
if s == value {
return true
}
}
return false
}
type fakePolicyRuleEvaluator struct {
level audit.Level
stage []audit.Stage
}
func (f *fakePolicyRuleEvaluator) EvaluatePolicyRule(_ authorizer.Attributes) auditinternal.RequestAuditConfig {
return auditinternal.RequestAuditConfig{
Level: f.level,
OmitStages: f.stage,
}
}

View File

@ -1,101 +0,0 @@
/*
Copyright 2017 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 policy
import (
"fmt"
"io/ioutil"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
"k8s.io/apiserver/pkg/apis/audit/validation"
"k8s.io/apiserver/pkg/audit"
"k8s.io/klog/v2"
)
var (
apiGroupVersions = []schema.GroupVersion{
auditv1.SchemeGroupVersion,
}
apiGroupVersionSet = map[schema.GroupVersion]bool{}
)
func init() {
for _, gv := range apiGroupVersions {
apiGroupVersionSet[gv] = true
}
}
func LoadPolicyFromFile(filePath string) (*auditinternal.Policy, error) {
if filePath == "" {
return nil, fmt.Errorf("file path not specified")
}
policyDef, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file path %q: %+v", filePath, err)
}
ret, err := LoadPolicyFromBytes(policyDef)
if err != nil {
return nil, fmt.Errorf("%v: from file %v", err.Error(), filePath)
}
return ret, nil
}
func LoadPolicyFromBytes(policyDef []byte) (*auditinternal.Policy, error) {
policy := &auditinternal.Policy{}
strictDecoder := serializer.NewCodecFactory(audit.Scheme, serializer.EnableStrict).UniversalDecoder()
// Try strict decoding first.
_, gvk, err := strictDecoder.Decode(policyDef, nil, policy)
if err != nil {
if !runtime.IsStrictDecodingError(err) {
return nil, fmt.Errorf("failed decoding: %w", err)
}
var (
lenientDecoder = audit.Codecs.UniversalDecoder(apiGroupVersions...)
lenientErr error
)
_, gvk, lenientErr = lenientDecoder.Decode(policyDef, nil, policy)
if lenientErr != nil {
return nil, fmt.Errorf("failed lenient decoding: %w", lenientErr)
}
klog.Warningf("Audit policy contains errors, falling back to lenient decoding: %v", err)
}
// Ensure the policy file contained an apiVersion and kind.
gv := schema.GroupVersion{Group: gvk.Group, Version: gvk.Version}
if !apiGroupVersionSet[gv] {
return nil, fmt.Errorf("unknown group version field %v in policy", gvk)
}
if err := validation.ValidatePolicy(policy); err != nil {
return nil, err.ToAggregate()
}
policyCnt := len(policy.Rules)
if policyCnt == 0 {
return nil, fmt.Errorf("loaded illegal policy with 0 rules")
}
klog.V(4).InfoS("Load audit policy rules success", "policyCnt", policyCnt)
return policy, nil
}

View File

@ -1,68 +0,0 @@
/*
Copyright 2018 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 policy
import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/apis/audit"
)
// AllStages returns all possible stages
func AllStages() sets.String {
return sets.NewString(
string(audit.StageRequestReceived),
string(audit.StageResponseStarted),
string(audit.StageResponseComplete),
string(audit.StagePanic),
)
}
// AllLevels returns all possible levels
func AllLevels() sets.String {
return sets.NewString(
string(audit.LevelNone),
string(audit.LevelMetadata),
string(audit.LevelRequest),
string(audit.LevelRequestResponse),
)
}
// InvertStages subtracts the given array of stages from all stages
func InvertStages(stages []audit.Stage) []audit.Stage {
s := ConvertStagesToStrings(stages)
a := AllStages()
a.Delete(s...)
return ConvertStringSetToStages(a)
}
// ConvertStagesToStrings converts an array of stages to a string array
func ConvertStagesToStrings(stages []audit.Stage) []string {
s := make([]string, len(stages))
for i, stage := range stages {
s[i] = string(stage)
}
return s
}
// ConvertStringSetToStages converts a string set to an array of stages
func ConvertStringSetToStages(set sets.String) []audit.Stage {
stages := make([]audit.Stage, len(set))
for i, stage := range set.List() {
stages[i] = audit.Stage(stage)
}
return stages
}

View File

@ -1,90 +0,0 @@
/*
Copyright 2018 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 authenticator
import (
"context"
"fmt"
"net/http"
)
func authenticate(ctx context.Context, implicitAuds Audiences, authenticate func() (*Response, bool, error)) (*Response, bool, error) {
targetAuds, ok := AudiencesFrom(ctx)
// We can remove this once api audiences is never empty. That will probably
// be N releases after TokenRequest is GA.
if !ok {
return authenticate()
}
auds := implicitAuds.Intersect(targetAuds)
if len(auds) == 0 {
return nil, false, nil
}
resp, ok, err := authenticate()
if err != nil || !ok {
return nil, false, err
}
if len(resp.Audiences) > 0 {
// maybe the authenticator was audience aware after all.
return nil, false, fmt.Errorf("audience agnostic authenticator wrapped an authenticator that returned audiences: %q", resp.Audiences)
}
resp.Audiences = auds
return resp, true, nil
}
type audAgnosticRequestAuthenticator struct {
implicit Audiences
delegate Request
}
var _ = Request(&audAgnosticRequestAuthenticator{})
func (a *audAgnosticRequestAuthenticator) AuthenticateRequest(req *http.Request) (*Response, bool, error) {
return authenticate(req.Context(), a.implicit, func() (*Response, bool, error) {
return a.delegate.AuthenticateRequest(req)
})
}
// WrapAudienceAgnosticRequest wraps an audience agnostic request authenticator
// to restrict its accepted audiences to a set of implicit audiences.
func WrapAudienceAgnosticRequest(implicit Audiences, delegate Request) Request {
return &audAgnosticRequestAuthenticator{
implicit: implicit,
delegate: delegate,
}
}
type audAgnosticTokenAuthenticator struct {
implicit Audiences
delegate Token
}
var _ = Token(&audAgnosticTokenAuthenticator{})
func (a *audAgnosticTokenAuthenticator) AuthenticateToken(ctx context.Context, tok string) (*Response, bool, error) {
return authenticate(ctx, a.implicit, func() (*Response, bool, error) {
return a.delegate.AuthenticateToken(ctx, tok)
})
}
// WrapAudienceAgnosticToken wraps an audience agnostic token authenticator to
// restrict its accepted audiences to a set of implicit audiences.
func WrapAudienceAgnosticToken(implicit Audiences, delegate Token) Token {
return &audAgnosticTokenAuthenticator{
implicit: implicit,
delegate: delegate,
}
}

View File

@ -1,63 +0,0 @@
/*
Copyright 2018 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 authenticator
import "context"
// Audiences is a container for the Audiences of a token.
type Audiences []string
// The key type is unexported to prevent collisions
type key int
const (
// audiencesKey is the context key for request audiences.
audiencesKey key = iota
)
// WithAudiences returns a context that stores a request's expected audiences.
func WithAudiences(ctx context.Context, auds Audiences) context.Context {
return context.WithValue(ctx, audiencesKey, auds)
}
// AudiencesFrom returns a request's expected audiences stored in the request context.
func AudiencesFrom(ctx context.Context) (Audiences, bool) {
auds, ok := ctx.Value(audiencesKey).(Audiences)
return auds, ok
}
// Has checks if Audiences contains a specific audiences.
func (a Audiences) Has(taud string) bool {
for _, aud := range a {
if aud == taud {
return true
}
}
return false
}
// Intersect intersects Audiences with a target Audiences and returns all
// elements in both.
func (a Audiences) Intersect(tauds Audiences) Audiences {
selected := Audiences{}
for _, taud := range tauds {
if a.Has(taud) {
selected = append(selected, taud)
}
}
return selected
}

View File

@ -1,65 +0,0 @@
/*
Copyright 2014 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 authenticator
import (
"context"
"net/http"
"k8s.io/apiserver/pkg/authentication/user"
)
// Token checks a string value against a backing authentication store and
// returns a Response or an error if the token could not be checked.
type Token interface {
AuthenticateToken(ctx context.Context, token string) (*Response, bool, error)
}
// Request attempts to extract authentication information from a request and
// returns a Response or an error if the request could not be checked.
type Request interface {
AuthenticateRequest(req *http.Request) (*Response, bool, error)
}
// TokenFunc is a function that implements the Token interface.
type TokenFunc func(ctx context.Context, token string) (*Response, bool, error)
// AuthenticateToken implements authenticator.Token.
func (f TokenFunc) AuthenticateToken(ctx context.Context, token string) (*Response, bool, error) {
return f(ctx, token)
}
// RequestFunc is a function that implements the Request interface.
type RequestFunc func(req *http.Request) (*Response, bool, error)
// AuthenticateRequest implements authenticator.Request.
func (f RequestFunc) AuthenticateRequest(req *http.Request) (*Response, bool, error) {
return f(req)
}
// Response is the struct returned by authenticator interfaces upon successful
// authentication. It contains information about whether the authenticator
// authenticated the request, information about the context of the
// authentication, and information about the authenticated user.
type Response struct {
// Audiences is the set of audiences the authenticator was able to validate
// the token against. If the authenticator is not audience aware, this field
// will be empty.
Audiences Audiences
// User is the UserInfo associated with the authentication context.
User user.Info
}

View File

@ -1,128 +0,0 @@
/*
Copyright 2016 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 authenticatorfactory
import (
"errors"
"time"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/group"
"k8s.io/apiserver/pkg/authentication/request/anonymous"
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
unionauth "k8s.io/apiserver/pkg/authentication/request/union"
"k8s.io/apiserver/pkg/authentication/request/websocket"
"k8s.io/apiserver/pkg/authentication/request/x509"
"k8s.io/apiserver/pkg/authentication/token/cache"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// DelegatingAuthenticatorConfig is the minimal configuration needed to create an authenticator
// built to delegate authentication to a kube API server
type DelegatingAuthenticatorConfig struct {
Anonymous *apiserver.AnonymousAuthConfig
// TokenAccessReviewClient is a client to do token review. It can be nil. Then every token is ignored.
TokenAccessReviewClient authenticationclient.AuthenticationV1Interface
// TokenAccessReviewTimeout specifies a time limit for requests made by the authorization webhook client.
TokenAccessReviewTimeout time.Duration
// WebhookRetryBackoff specifies the backoff parameters for the authentication webhook retry logic.
// This allows us to configure the sleep time at each iteration and the maximum number of retries allowed
// before we fail the webhook call in order to limit the fan out that ensues when the system is degraded.
WebhookRetryBackoff *wait.Backoff
// CacheTTL is the length of time that a token authentication answer will be cached.
CacheTTL time.Duration
// CAContentProvider are the options for verifying incoming connections using mTLS and directly assigning to users.
// Generally this is the CA bundle file used to authenticate client certificates
// If this is nil, then mTLS will not be used.
ClientCertificateCAContentProvider dynamiccertificates.CAContentProvider
APIAudiences authenticator.Audiences
RequestHeaderConfig *RequestHeaderConfig
}
func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDefinitions, error) {
authenticators := []authenticator.Request{}
securityDefinitions := spec.SecurityDefinitions{}
// front-proxy first, then remote
// Add the front proxy authenticator if requested
if c.RequestHeaderConfig != nil {
requestHeaderAuthenticator := headerrequest.NewDynamicVerifyOptionsSecure(
c.RequestHeaderConfig.CAContentProvider.VerifyOptions,
c.RequestHeaderConfig.AllowedClientNames,
c.RequestHeaderConfig.UsernameHeaders,
c.RequestHeaderConfig.UIDHeaders,
c.RequestHeaderConfig.GroupHeaders,
c.RequestHeaderConfig.ExtraHeaderPrefixes,
)
authenticators = append(authenticators, requestHeaderAuthenticator)
}
// x509 client cert auth
if c.ClientCertificateCAContentProvider != nil {
authenticators = append(authenticators, x509.NewDynamic(c.ClientCertificateCAContentProvider.VerifyOptions, x509.CommonNameUserConversion))
}
if c.TokenAccessReviewClient != nil {
if c.WebhookRetryBackoff == nil {
return nil, nil, errors.New("retry backoff parameters for delegating authentication webhook has not been specified")
}
tokenAuth, err := webhooktoken.NewFromInterface(c.TokenAccessReviewClient, c.APIAudiences, *c.WebhookRetryBackoff, c.TokenAccessReviewTimeout, webhooktoken.AuthenticatorMetrics{
RecordRequestTotal: RecordRequestTotal,
RecordRequestLatency: RecordRequestLatency,
})
if err != nil {
return nil, nil, err
}
cachingTokenAuth := cache.New(tokenAuth, false, c.CacheTTL, c.CacheTTL)
authenticators = append(authenticators, bearertoken.New(cachingTokenAuth), websocket.NewProtocolAuthenticator(cachingTokenAuth))
securityDefinitions["BearerToken"] = &spec.SecurityScheme{
SecuritySchemeProps: spec.SecuritySchemeProps{
Type: "apiKey",
Name: "authorization",
In: "header",
Description: "Bearer Token authentication",
},
}
}
if len(authenticators) == 0 {
if c.Anonymous != nil && c.Anonymous.Enabled {
return anonymous.NewAuthenticator(c.Anonymous.Conditions), &securityDefinitions, nil
}
return nil, nil, errors.New("no authentication method configured")
}
authenticator := group.NewAuthenticatedGroupAdder(unionauth.New(authenticators...))
if c.Anonymous != nil && c.Anonymous.Enabled {
authenticator = unionauth.NewFailOnError(authenticator, anonymous.NewAuthenticator(c.Anonymous.Conditions))
}
return authenticator, &securityDefinitions, nil
}

View File

@ -1,29 +0,0 @@
/*
Copyright 2016 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 authenticatorfactory
import (
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
"k8s.io/apiserver/pkg/authentication/token/tokenfile"
"k8s.io/apiserver/pkg/authentication/user"
)
// NewFromTokens returns an authenticator.Request or an error
func NewFromTokens(tokens map[string]*user.DefaultInfo, audiences authenticator.Audiences) authenticator.Request {
return bearertoken.New(authenticator.WrapAudienceAgnosticToken(audiences, tokenfile.New(tokens)))
}

View File

@ -1,69 +0,0 @@
/*
Copyright 2021 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 authenticatorfactory
import (
"context"
compbasemetrics "k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
type registerables []compbasemetrics.Registerable
// init registers all metrics.
func init() {
for _, metric := range metrics {
legacyregistry.MustRegister(metric)
}
}
var (
requestTotal = compbasemetrics.NewCounterVec(
&compbasemetrics.CounterOpts{
Name: "apiserver_delegated_authn_request_total",
Help: "Number of HTTP requests partitioned by status code.",
StabilityLevel: compbasemetrics.ALPHA,
},
[]string{"code"},
)
requestLatency = compbasemetrics.NewHistogramVec(
&compbasemetrics.HistogramOpts{
Name: "apiserver_delegated_authn_request_duration_seconds",
Help: "Request latency in seconds. Broken down by status code.",
Buckets: []float64{0.25, 0.5, 0.7, 1, 1.5, 3, 5, 10},
StabilityLevel: compbasemetrics.ALPHA,
},
[]string{"code"},
)
metrics = registerables{
requestTotal,
requestLatency,
}
)
// RecordRequestTotal increments the total number of requests for the delegated authentication.
func RecordRequestTotal(ctx context.Context, code string) {
requestTotal.WithContext(ctx).WithLabelValues(code).Inc()
}
// RecordRequestLatency measures request latency in seconds for the delegated authentication. Broken down by status code.
func RecordRequestLatency(ctx context.Context, code string, latency float64) {
requestLatency.WithContext(ctx).WithLabelValues(code).Observe(latency)
}

View File

@ -1,39 +0,0 @@
/*
Copyright 2014 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 authenticatorfactory
import (
"k8s.io/apiserver/pkg/authentication/request/headerrequest"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
)
type RequestHeaderConfig struct {
// UsernameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
UsernameHeaders headerrequest.StringSliceProvider
// UsernameHeaders are the headers to check (in order, case-insensitively) for an identity UID. The first header with a value wins.
UIDHeaders headerrequest.StringSliceProvider
// GroupHeaders are the headers to check (case-insensitively) for a group names. All values will be used.
GroupHeaders headerrequest.StringSliceProvider
// ExtraHeaderPrefixes are the head prefixes to check (case-insentively) for filling in
// the user.Info.Extra. All values of all matching headers will be added.
ExtraHeaderPrefixes headerrequest.StringSliceProvider
// CAContentProvider the options for verifying incoming connections using mTLS. Generally this points to CA bundle file which is used verify the identity of the front proxy.
// It may produce different options at will.
CAContentProvider dynamiccertificates.CAContentProvider
// AllowedClientNames is a list of common names that may be presented by the authenticating front proxy. Empty means: accept any.
AllowedClientNames headerrequest.StringSliceProvider
}

View File

@ -1,161 +0,0 @@
/*
Copyright 2023 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 cel
import (
"fmt"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/version"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
)
const (
claimsVarName = "claims"
userVarName = "user"
)
// compiler implements the Compiler interface.
type compiler struct {
// varEnvs is a map of CEL environments, keyed by the name of the CEL variable.
// The CEL variable is available to the expression.
// We have 2 environments, one for claims and one for user.
varEnvs map[string]*environment.EnvSet
}
// NewDefaultCompiler returns a new Compiler following the default compatibility version.
// Note: the compiler construction depends on feature gates and the compatibility version to be initialized.
func NewDefaultCompiler() Compiler {
return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
}
// NewCompiler returns a new Compiler.
func NewCompiler(env *environment.EnvSet) Compiler {
return &compiler{
varEnvs: mustBuildEnvs(env),
}
}
// CompileClaimsExpression compiles the given expressionAccessor into a CEL program that can be evaluated.
// The claims CEL variable is available to the expression.
func (c compiler) CompileClaimsExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error) {
return c.compile(expressionAccessor, claimsVarName)
}
// CompileUserExpression compiles the given expressionAccessor into a CEL program that can be evaluated.
// The user CEL variable is available to the expression.
func (c compiler) CompileUserExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error) {
return c.compile(expressionAccessor, userVarName)
}
func (c compiler) compile(expressionAccessor ExpressionAccessor, envVarName string) (CompilationResult, error) {
resultError := func(errorString string, errType apiservercel.ErrorType) (CompilationResult, error) {
return CompilationResult{}, &apiservercel.Error{
Type: errType,
Detail: errorString,
}
}
env, err := c.varEnvs[envVarName].Env(environment.StoredExpressions)
if err != nil {
return resultError(fmt.Sprintf("unexpected error loading CEL environment: %v", err), apiservercel.ErrorTypeInternal)
}
ast, issues := env.Compile(expressionAccessor.GetExpression())
if issues != nil {
return resultError("compilation failed: "+issues.String(), apiservercel.ErrorTypeInvalid)
}
found := false
returnTypes := expressionAccessor.ReturnTypes()
for _, returnType := range returnTypes {
if ast.OutputType() == returnType || cel.AnyType == returnType {
found = true
break
}
}
if !found {
var reason string
if len(returnTypes) == 1 {
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
} else {
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
}
return resultError(reason, apiservercel.ErrorTypeInvalid)
}
if _, err = cel.AstToCheckedExpr(ast); err != nil {
// should be impossible since env.Compile returned no issues
return resultError("unexpected compilation error: "+err.Error(), apiservercel.ErrorTypeInternal)
}
prog, err := env.Program(ast)
if err != nil {
return resultError("program instantiation failed: "+err.Error(), apiservercel.ErrorTypeInternal)
}
return CompilationResult{
Program: prog,
AST: ast,
ExpressionAccessor: expressionAccessor,
}, nil
}
func buildUserType() *apiservercel.DeclType {
field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
return apiservercel.NewDeclField(name, declType, required, nil, nil)
}
fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField {
result := make(map[string]*apiservercel.DeclField, len(fields))
for _, f := range fields {
result[f.Name] = f
}
return result
}
return apiservercel.NewObjectType("kubernetes.UserInfo", fields(
field("username", apiservercel.StringType, false),
field("uid", apiservercel.StringType, false),
field("groups", apiservercel.NewListType(apiservercel.StringType, -1), false),
field("extra", apiservercel.NewMapType(apiservercel.StringType, apiservercel.NewListType(apiservercel.StringType, -1), -1), false),
))
}
func mustBuildEnvs(baseEnv *environment.EnvSet) map[string]*environment.EnvSet {
buildEnvSet := func(envOpts []cel.EnvOption, declTypes []*apiservercel.DeclType) *environment.EnvSet {
env, err := baseEnv.Extend(environment.VersionedOptions{
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: declTypes,
})
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
}
return env
}
userType := buildUserType()
claimsType := apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, -1)
envs := make(map[string]*environment.EnvSet, 2) // build two environments, one for claims and one for user
envs[claimsVarName] = buildEnvSet([]cel.EnvOption{cel.Variable(claimsVarName, claimsType.CelType())}, []*apiservercel.DeclType{claimsType})
envs[userVarName] = buildEnvSet([]cel.EnvOption{cel.Variable(userVarName, userType.CelType())}, []*apiservercel.DeclType{userType})
return envs
}

View File

@ -1,148 +0,0 @@
/*
Copyright 2023 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 cel contains the CEL related interfaces and structs for authentication.
package cel
import (
"context"
celgo "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// ExpressionAccessor is an interface that provides access to a CEL expression.
type ExpressionAccessor interface {
GetExpression() string
ReturnTypes() []*celgo.Type
}
// CompilationResult represents a compiled validations expression.
type CompilationResult struct {
Program celgo.Program
AST *celgo.Ast
ExpressionAccessor ExpressionAccessor
}
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
type EvaluationResult struct {
EvalResult ref.Val
ExpressionAccessor ExpressionAccessor
}
// Compiler provides a CEL expression compiler configured with the desired authentication related CEL variables.
type Compiler interface {
CompileClaimsExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error)
CompileUserExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error)
}
// ClaimsMapper provides a CEL expression mapper configured with the claims CEL variable.
type ClaimsMapper interface {
// EvalClaimMapping evaluates the given claim mapping expression and returns a EvaluationResult.
// This is used for username, groups and uid claim mapping that contains a single expression.
EvalClaimMapping(ctx context.Context, claims *unstructured.Unstructured) (EvaluationResult, error)
// EvalClaimMappings evaluates the given expressions and returns a list of EvaluationResult.
// This is used for extra claim mapping and claim validation that contains a list of expressions.
EvalClaimMappings(ctx context.Context, claims *unstructured.Unstructured) ([]EvaluationResult, error)
}
// UserMapper provides a CEL expression mapper configured with the user CEL variable.
type UserMapper interface {
// EvalUser evaluates the given user expressions and returns a list of EvaluationResult.
// This is used for user validation that contains a list of expressions.
EvalUser(ctx context.Context, userInfo *unstructured.Unstructured) ([]EvaluationResult, error)
}
var _ ExpressionAccessor = &ClaimMappingExpression{}
// ClaimMappingExpression is a CEL expression that maps a claim.
type ClaimMappingExpression struct {
Expression string
}
// GetExpression returns the CEL expression.
func (v *ClaimMappingExpression) GetExpression() string {
return v.Expression
}
// ReturnTypes returns the CEL expression return types.
func (v *ClaimMappingExpression) ReturnTypes() []*celgo.Type {
// return types is only used for validation. The claims variable that's available
// to the claim mapping expressions is a map[string]interface{}, so we can't
// really know what the return type is during compilation. Strict type checking
// is done during evaluation.
return []*celgo.Type{celgo.AnyType}
}
var _ ExpressionAccessor = &ClaimValidationCondition{}
// ClaimValidationCondition is a CEL expression that validates a claim.
type ClaimValidationCondition struct {
Expression string
Message string
}
// GetExpression returns the CEL expression.
func (v *ClaimValidationCondition) GetExpression() string {
return v.Expression
}
// ReturnTypes returns the CEL expression return types.
func (v *ClaimValidationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
var _ ExpressionAccessor = &ExtraMappingExpression{}
// ExtraMappingExpression is a CEL expression that maps an extra to a list of values.
type ExtraMappingExpression struct {
Key string
Expression string
}
// GetExpression returns the CEL expression.
func (v *ExtraMappingExpression) GetExpression() string {
return v.Expression
}
// ReturnTypes returns the CEL expression return types.
func (v *ExtraMappingExpression) ReturnTypes() []*celgo.Type {
// return types is only used for validation. The claims variable that's available
// to the claim mapping expressions is a map[string]interface{}, so we can't
// really know what the return type is during compilation. Strict type checking
// is done during evaluation.
return []*celgo.Type{celgo.AnyType}
}
var _ ExpressionAccessor = &UserValidationCondition{}
// UserValidationCondition is a CEL expression that validates a User.
type UserValidationCondition struct {
Expression string
Message string
}
// GetExpression returns the CEL expression.
func (v *UserValidationCondition) GetExpression() string {
return v.Expression
}
// ReturnTypes returns the CEL expression return types.
func (v *UserValidationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}

View File

@ -1,97 +0,0 @@
/*
Copyright 2023 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 cel
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
var _ ClaimsMapper = &mapper{}
var _ UserMapper = &mapper{}
// mapper implements the ClaimsMapper and UserMapper interface.
type mapper struct {
compilationResults []CompilationResult
}
// CELMapper is a struct that holds the compiled expressions for
// username, groups, uid, extra, claimValidation and userValidation
type CELMapper struct {
Username ClaimsMapper
Groups ClaimsMapper
UID ClaimsMapper
Extra ClaimsMapper
ClaimValidationRules ClaimsMapper
UserValidationRules UserMapper
}
// NewClaimsMapper returns a new ClaimsMapper.
func NewClaimsMapper(compilationResults []CompilationResult) ClaimsMapper {
return &mapper{
compilationResults: compilationResults,
}
}
// NewUserMapper returns a new UserMapper.
func NewUserMapper(compilationResults []CompilationResult) UserMapper {
return &mapper{
compilationResults: compilationResults,
}
}
// EvalClaimMapping evaluates the given claim mapping expression and returns a EvaluationResult.
func (m *mapper) EvalClaimMapping(ctx context.Context, claims *unstructured.Unstructured) (EvaluationResult, error) {
results, err := m.eval(ctx, map[string]interface{}{claimsVarName: claims.Object})
if err != nil {
return EvaluationResult{}, err
}
if len(results) != 1 {
return EvaluationResult{}, fmt.Errorf("expected 1 evaluation result, got %d", len(results))
}
return results[0], nil
}
// EvalClaimMappings evaluates the given expressions and returns a list of EvaluationResult.
func (m *mapper) EvalClaimMappings(ctx context.Context, claims *unstructured.Unstructured) ([]EvaluationResult, error) {
return m.eval(ctx, map[string]interface{}{claimsVarName: claims.Object})
}
// EvalUser evaluates the given user expressions and returns a list of EvaluationResult.
func (m *mapper) EvalUser(ctx context.Context, userInfo *unstructured.Unstructured) ([]EvaluationResult, error) {
return m.eval(ctx, map[string]interface{}{userVarName: userInfo.Object})
}
func (m *mapper) eval(ctx context.Context, input map[string]interface{}) ([]EvaluationResult, error) {
evaluations := make([]EvaluationResult, len(m.compilationResults))
for i, compilationResult := range m.compilationResults {
var evaluation = &evaluations[i]
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
evalResult, _, err := compilationResult.Program.ContextEval(ctx, input)
if err != nil {
return nil, fmt.Errorf("expression '%s' resulted in error: %w", compilationResult.ExpressionAccessor.GetExpression(), err)
}
evaluation.EvalResult = evalResult
}
return evaluations, nil
}

View File

@ -1,66 +0,0 @@
/*
Copyright 2017 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 group
import (
"net/http"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
)
// AuthenticatedGroupAdder adds system:authenticated group when appropriate
type AuthenticatedGroupAdder struct {
// Authenticator is delegated to make the authentication decision
Authenticator authenticator.Request
}
// NewAuthenticatedGroupAdder wraps a request authenticator, and adds the system:authenticated group when appropriate.
// Authentication must succeed, the user must not be system:anonymous, the groups system:authenticated or system:unauthenticated must
// not be present
func NewAuthenticatedGroupAdder(auth authenticator.Request) authenticator.Request {
return &AuthenticatedGroupAdder{auth}
}
func (g *AuthenticatedGroupAdder) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
r, ok, err := g.Authenticator.AuthenticateRequest(req)
if err != nil || !ok {
return nil, ok, err
}
if r.User.GetName() == user.Anonymous {
return r, true, nil
}
for _, group := range r.User.GetGroups() {
if group == user.AllAuthenticated || group == user.AllUnauthenticated {
return r, true, nil
}
}
newGroups := make([]string, 0, len(r.User.GetGroups())+1)
newGroups = append(newGroups, r.User.GetGroups()...)
newGroups = append(newGroups, user.AllAuthenticated)
ret := *r // shallow copy
ret.User = &user.DefaultInfo{
Name: r.User.GetName(),
UID: r.User.GetUID(),
Groups: newGroups,
Extra: r.User.GetExtra(),
}
return &ret, true, nil
}

View File

@ -1,57 +0,0 @@
/*
Copyright 2016 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 group
import (
"net/http"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
)
// GroupAdder adds groups to an authenticated user.Info
type GroupAdder struct {
// Authenticator is delegated to make the authentication decision
Authenticator authenticator.Request
// Groups are additional groups to add to the user.Info from a successful authentication
Groups []string
}
// NewGroupAdder wraps a request authenticator, and adds the specified groups to the returned user when authentication succeeds
func NewGroupAdder(auth authenticator.Request, groups []string) authenticator.Request {
return &GroupAdder{auth, groups}
}
func (g *GroupAdder) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
r, ok, err := g.Authenticator.AuthenticateRequest(req)
if err != nil || !ok {
return nil, ok, err
}
newGroups := make([]string, 0, len(r.User.GetGroups())+len(g.Groups))
newGroups = append(newGroups, r.User.GetGroups()...)
newGroups = append(newGroups, g.Groups...)
ret := *r // shallow copy
ret.User = &user.DefaultInfo{
Name: r.User.GetName(),
UID: r.User.GetUID(),
Groups: newGroups,
Extra: r.User.GetExtra(),
}
return &ret, true, nil
}

View File

@ -1,57 +0,0 @@
/*
Copyright 2017 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 group
import (
"context"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
)
// TokenGroupAdder adds groups to an authenticated user.Info
type TokenGroupAdder struct {
// Authenticator is delegated to make the authentication decision
Authenticator authenticator.Token
// Groups are additional groups to add to the user.Info from a successful authentication
Groups []string
}
// NewTokenGroupAdder wraps a token authenticator, and adds the specified groups to the returned user when authentication succeeds
func NewTokenGroupAdder(auth authenticator.Token, groups []string) authenticator.Token {
return &TokenGroupAdder{auth, groups}
}
func (g *TokenGroupAdder) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
r, ok, err := g.Authenticator.AuthenticateToken(ctx, token)
if err != nil || !ok {
return nil, ok, err
}
newGroups := make([]string, 0, len(r.User.GetGroups())+len(g.Groups))
newGroups = append(newGroups, r.User.GetGroups()...)
newGroups = append(newGroups, g.Groups...)
ret := *r // shallow copy
ret.User = &user.DefaultInfo{
Name: r.User.GetName(),
UID: r.User.GetUID(),
Groups: newGroups,
Extra: r.User.GetExtra(),
}
return &ret, true, nil
}

View File

@ -1,62 +0,0 @@
/*
Copyright 2016 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 anonymous
import (
"net/http"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
)
const (
anonymousUser = user.Anonymous
unauthenticatedGroup = user.AllUnauthenticated
)
type Authenticator struct {
allowedPaths map[string]bool
}
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
if len(a.allowedPaths) > 0 && !a.allowedPaths[req.URL.Path] {
return nil, false, nil
}
auds, _ := authenticator.AudiencesFrom(req.Context())
return &authenticator.Response{
User: &user.DefaultInfo{
Name: anonymousUser,
Groups: []string{unauthenticatedGroup},
},
Audiences: auds,
}, true, nil
}
// NewAuthenticator returns a new anonymous authenticator.
// When conditions is empty all requests are authenticated as anonymous.
// When conditions are non-empty only those requests that match the at-least one
// condition are authenticated as anonymous.
func NewAuthenticator(conditions []apiserver.AnonymousAuthCondition) authenticator.Request {
allowedPaths := make(map[string]bool)
for _, c := range conditions {
allowedPaths[c.Path] = true
}
return &Authenticator{allowedPaths: allowedPaths}
}

View File

@ -1,76 +0,0 @@
/*
Copyright 2014 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 bearertoken
import (
"errors"
"net/http"
"strings"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/warning"
)
const (
invalidTokenWithSpaceWarning = "the provided Authorization header contains extra space before the bearer token, and is ignored"
)
type Authenticator struct {
auth authenticator.Token
}
func New(auth authenticator.Token) *Authenticator {
return &Authenticator{auth}
}
var invalidToken = errors.New("invalid bearer token")
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
auth := strings.TrimSpace(req.Header.Get("Authorization"))
if auth == "" {
return nil, false, nil
}
parts := strings.SplitN(auth, " ", 3)
if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
return nil, false, nil
}
token := parts[1]
// Empty bearer tokens aren't valid
if len(token) == 0 {
// The space before the token case
if len(parts) == 3 {
warning.AddWarning(req.Context(), "", invalidTokenWithSpaceWarning)
}
return nil, false, nil
}
resp, ok, err := a.auth.AuthenticateToken(req.Context(), token)
// if we authenticated successfully, go ahead and remove the bearer token so that no one
// is ever tempted to use it inside of the API server
if ok {
req.Header.Del("Authorization")
}
// If the token authenticator didn't error, provide a default error
if !ok && err == nil {
err = invalidToken
}
return resp, ok, err
}

View File

@ -1,218 +0,0 @@
/*
Copyright 2016 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 headerrequest
import (
"fmt"
"net/http"
"net/url"
"strings"
"k8s.io/apiserver/pkg/authentication/authenticator"
x509request "k8s.io/apiserver/pkg/authentication/request/x509"
"k8s.io/apiserver/pkg/authentication/user"
)
// StringSliceProvider is a way to get a string slice value. It is heavily used for authentication headers among other places.
type StringSliceProvider interface {
// Value returns the current string slice. Callers should never mutate the returned value.
Value() []string
}
// StringSliceProviderFunc is a function that matches the StringSliceProvider interface
type StringSliceProviderFunc func() []string
// Value returns the current string slice. Callers should never mutate the returned value.
func (d StringSliceProviderFunc) Value() []string {
return d()
}
// StaticStringSlice a StringSliceProvider that returns a fixed value
type StaticStringSlice []string
// Value returns the current string slice. Callers should never mutate the returned value.
func (s StaticStringSlice) Value() []string {
return s
}
type requestHeaderAuthRequestHandler struct {
// nameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins.
nameHeaders StringSliceProvider
// nameHeaders are the headers to check (in order, case-insensitively) for an identity UID. The first header with a value wins.
uidHeaders StringSliceProvider
// groupHeaders are the headers to check (case-insensitively) for group membership. All values of all headers will be added.
groupHeaders StringSliceProvider
// extraHeaderPrefixes are the head prefixes to check (case-insensitively) for filling in
// the user.Info.Extra. All values of all matching headers will be added.
extraHeaderPrefixes StringSliceProvider
}
func New(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes []string) (authenticator.Request, error) {
trimmedNameHeaders, err := trimHeaders(nameHeaders...)
if err != nil {
return nil, err
}
trimmedUIDHeaders, err := trimHeaders(uidHeaders...)
if err != nil {
return nil, err
}
trimmedGroupHeaders, err := trimHeaders(groupHeaders...)
if err != nil {
return nil, err
}
trimmedExtraHeaderPrefixes, err := trimHeaders(extraHeaderPrefixes...)
if err != nil {
return nil, err
}
return NewDynamic(
StaticStringSlice(trimmedNameHeaders),
StaticStringSlice(trimmedUIDHeaders),
StaticStringSlice(trimmedGroupHeaders),
StaticStringSlice(trimmedExtraHeaderPrefixes),
), nil
}
func NewDynamic(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request {
return &requestHeaderAuthRequestHandler{
nameHeaders: nameHeaders,
uidHeaders: uidHeaders,
groupHeaders: groupHeaders,
extraHeaderPrefixes: extraHeaderPrefixes,
}
}
func trimHeaders(headerNames ...string) ([]string, error) {
ret := []string{}
for _, headerName := range headerNames {
trimmedHeader := strings.TrimSpace(headerName)
if len(trimmedHeader) == 0 {
return nil, fmt.Errorf("empty header %q", headerName)
}
ret = append(ret, trimmedHeader)
}
return ret, nil
}
func NewDynamicVerifyOptionsSecure(verifyOptionFn x509request.VerifyOptionFunc, proxyClientNames, nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request {
headerAuthenticator := NewDynamic(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes)
return x509request.NewDynamicCAVerifier(verifyOptionFn, headerAuthenticator, proxyClientNames)
}
func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
name := headerValue(req.Header, a.nameHeaders.Value())
if len(name) == 0 {
return nil, false, nil
}
uid := headerValue(req.Header, a.uidHeaders.Value())
groups := allHeaderValues(req.Header, a.groupHeaders.Value())
extra := newExtra(req.Header, a.extraHeaderPrefixes.Value())
// clear headers used for authentication
ClearAuthenticationHeaders(req.Header, a.nameHeaders, a.uidHeaders, a.groupHeaders, a.extraHeaderPrefixes)
return &authenticator.Response{
User: &user.DefaultInfo{
Name: name,
UID: uid,
Groups: groups,
Extra: extra,
},
}, true, nil
}
func ClearAuthenticationHeaders(h http.Header, nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) {
for _, headerName := range nameHeaders.Value() {
h.Del(headerName)
}
for _, headerName := range uidHeaders.Value() {
h.Del(headerName)
}
for _, headerName := range groupHeaders.Value() {
h.Del(headerName)
}
for _, prefix := range extraHeaderPrefixes.Value() {
for k := range h {
if hasPrefixIgnoreCase(k, prefix) {
delete(h, k) // we have the raw key so avoid relying on canonicalization
}
}
}
}
func hasPrefixIgnoreCase(s, prefix string) bool {
return len(s) >= len(prefix) && strings.EqualFold(s[:len(prefix)], prefix)
}
func headerValue(h http.Header, headerNames []string) string {
for _, headerName := range headerNames {
headerValue := h.Get(headerName)
if len(headerValue) > 0 {
return headerValue
}
}
return ""
}
func allHeaderValues(h http.Header, headerNames []string) []string {
ret := []string{}
for _, headerName := range headerNames {
headerKey := http.CanonicalHeaderKey(headerName)
values, ok := h[headerKey]
if !ok {
continue
}
for _, headerValue := range values {
if len(headerValue) > 0 {
ret = append(ret, headerValue)
}
}
}
return ret
}
func unescapeExtraKey(encodedKey string) string {
key, err := url.PathUnescape(encodedKey) // Decode %-encoded bytes.
if err != nil {
return encodedKey // Always record extra strings, even if malformed/unencoded.
}
return key
}
func newExtra(h http.Header, headerPrefixes []string) map[string][]string {
ret := map[string][]string{}
// we have to iterate over prefixes first in order to have proper ordering inside the value slices
for _, prefix := range headerPrefixes {
for headerName, vv := range h {
if !hasPrefixIgnoreCase(headerName, prefix) {
continue
}
extraKey := unescapeExtraKey(strings.ToLower(headerName[len(prefix):]))
ret[extraKey] = append(ret[extraKey], vv...)
}
}
return ret
}

View File

@ -1,356 +0,0 @@
/*
Copyright 2020 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 headerrequest
import (
"context"
"encoding/json"
"fmt"
"sync/atomic"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
const (
authenticationRoleName = "extension-apiserver-authentication-reader"
)
// RequestHeaderAuthRequestProvider a provider that knows how to dynamically fill parts of RequestHeaderConfig struct
type RequestHeaderAuthRequestProvider interface {
UsernameHeaders() []string
UIDHeaders() []string
GroupHeaders() []string
ExtraHeaderPrefixes() []string
AllowedClientNames() []string
}
var _ RequestHeaderAuthRequestProvider = &RequestHeaderAuthRequestController{}
type requestHeaderBundle struct {
UsernameHeaders []string
UIDHeaders []string
GroupHeaders []string
ExtraHeaderPrefixes []string
AllowedClientNames []string
}
// RequestHeaderAuthRequestController a controller that exposes a set of methods for dynamically filling parts of RequestHeaderConfig struct.
// The methods are sourced from the config map which is being monitored by this controller.
// The controller is primed from the server at the construction time for components that don't want to dynamically react to changes
// in the config map.
type RequestHeaderAuthRequestController struct {
name string
configmapName string
configmapNamespace string
client kubernetes.Interface
configmapLister corev1listers.ConfigMapNamespaceLister
configmapInformer cache.SharedIndexInformer
configmapInformerSynced cache.InformerSynced
queue workqueue.TypedRateLimitingInterface[string]
// exportedRequestHeaderBundle is a requestHeaderBundle that contains the last read, non-zero length content of the configmap
exportedRequestHeaderBundle atomic.Value
usernameHeadersKey string
uidHeadersKey string
groupHeadersKey string
extraHeaderPrefixesKey string
allowedClientNamesKey string
}
// NewRequestHeaderAuthRequestController creates a new controller that implements RequestHeaderAuthRequestController
func NewRequestHeaderAuthRequestController(
cmName string,
cmNamespace string,
client kubernetes.Interface,
usernameHeadersKey, uidHeadersKey, groupHeadersKey, extraHeaderPrefixesKey, allowedClientNamesKey string) *RequestHeaderAuthRequestController {
c := &RequestHeaderAuthRequestController{
name: "RequestHeaderAuthRequestController",
client: client,
configmapName: cmName,
configmapNamespace: cmNamespace,
usernameHeadersKey: usernameHeadersKey,
uidHeadersKey: uidHeadersKey,
groupHeadersKey: groupHeadersKey,
extraHeaderPrefixesKey: extraHeaderPrefixesKey,
allowedClientNamesKey: allowedClientNamesKey,
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{Name: "RequestHeaderAuthRequestController"},
),
}
// we construct our own informer because we need such a small subset of the information available. Just one namespace.
c.configmapInformer = coreinformers.NewFilteredConfigMapInformer(client, c.configmapNamespace, 12*time.Hour, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, func(listOptions *metav1.ListOptions) {
listOptions.FieldSelector = fields.OneTermEqualSelector("metadata.name", c.configmapName).String()
})
c.configmapInformer.AddEventHandler(cache.FilteringResourceEventHandler{
FilterFunc: func(obj interface{}) bool {
if cast, ok := obj.(*corev1.ConfigMap); ok {
return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
}
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
if cast, ok := tombstone.Obj.(*corev1.ConfigMap); ok {
return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
}
}
return true // always return true just in case. The checks are fairly cheap
},
Handler: cache.ResourceEventHandlerFuncs{
// we have a filter, so any time we're called, we may as well queue. We only ever check one configmap
// so we don't have to be choosy about our key.
AddFunc: func(obj interface{}) {
c.queue.Add(c.keyFn())
},
UpdateFunc: func(oldObj, newObj interface{}) {
c.queue.Add(c.keyFn())
},
DeleteFunc: func(obj interface{}) {
c.queue.Add(c.keyFn())
},
},
})
c.configmapLister = corev1listers.NewConfigMapLister(c.configmapInformer.GetIndexer()).ConfigMaps(c.configmapNamespace)
c.configmapInformerSynced = c.configmapInformer.HasSynced
return c
}
func (c *RequestHeaderAuthRequestController) UsernameHeaders() []string {
return c.loadRequestHeaderFor(c.usernameHeadersKey)
}
func (c *RequestHeaderAuthRequestController) UIDHeaders() []string {
return c.loadRequestHeaderFor(c.uidHeadersKey)
}
func (c *RequestHeaderAuthRequestController) GroupHeaders() []string {
return c.loadRequestHeaderFor(c.groupHeadersKey)
}
func (c *RequestHeaderAuthRequestController) ExtraHeaderPrefixes() []string {
return c.loadRequestHeaderFor(c.extraHeaderPrefixesKey)
}
func (c *RequestHeaderAuthRequestController) AllowedClientNames() []string {
return c.loadRequestHeaderFor(c.allowedClientNamesKey)
}
// Run starts RequestHeaderAuthRequestController controller and blocks until stopCh is closed.
func (c *RequestHeaderAuthRequestController) Run(ctx context.Context, workers int) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.Infof("Starting %s", c.name)
defer klog.Infof("Shutting down %s", c.name)
go c.configmapInformer.Run(ctx.Done())
// wait for caches to fill before starting your work
if !cache.WaitForNamedCacheSync(c.name, ctx.Done(), c.configmapInformerSynced) {
return
}
// doesn't matter what workers say, only start one.
go wait.Until(c.runWorker, time.Second, ctx.Done())
<-ctx.Done()
}
// // RunOnce runs a single sync loop
func (c *RequestHeaderAuthRequestController) RunOnce(ctx context.Context) error {
configMap, err := c.client.CoreV1().ConfigMaps(c.configmapNamespace).Get(ctx, c.configmapName, metav1.GetOptions{})
switch {
case errors.IsNotFound(err):
// ignore, authConfigMap is nil now
return nil
case errors.IsForbidden(err):
klog.Warningf("Unable to get configmap/%s in %s. Usually fixed by "+
"'kubectl create rolebinding -n %s ROLEBINDING_NAME --role=%s --serviceaccount=YOUR_NS:YOUR_SA'",
c.configmapName, c.configmapNamespace, c.configmapNamespace, authenticationRoleName)
return err
case err != nil:
return err
}
return c.syncConfigMap(configMap)
}
func (c *RequestHeaderAuthRequestController) runWorker() {
for c.processNextWorkItem() {
}
}
func (c *RequestHeaderAuthRequestController) processNextWorkItem() bool {
dsKey, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(dsKey)
err := c.sync()
if err == nil {
c.queue.Forget(dsKey)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
c.queue.AddRateLimited(dsKey)
return true
}
// sync reads the config and propagates the changes to exportedRequestHeaderBundle
// which is exposed by the set of methods that are used to fill RequestHeaderConfig struct
func (c *RequestHeaderAuthRequestController) sync() error {
configMap, err := c.configmapLister.Get(c.configmapName)
if err != nil {
return err
}
return c.syncConfigMap(configMap)
}
func (c *RequestHeaderAuthRequestController) syncConfigMap(configMap *corev1.ConfigMap) error {
hasChanged, newRequestHeaderBundle, err := c.hasRequestHeaderBundleChanged(configMap)
if err != nil {
return err
}
if hasChanged {
c.exportedRequestHeaderBundle.Store(newRequestHeaderBundle)
klog.V(2).Infof("Loaded a new request header values for %v", c.name)
}
return nil
}
func (c *RequestHeaderAuthRequestController) hasRequestHeaderBundleChanged(cm *corev1.ConfigMap) (bool, *requestHeaderBundle, error) {
currentHeadersBundle, err := c.getRequestHeaderBundleFromConfigMap(cm)
if err != nil {
return false, nil, err
}
rawHeaderBundle := c.exportedRequestHeaderBundle.Load()
if rawHeaderBundle == nil {
return true, currentHeadersBundle, nil
}
// check to see if we have a change. If the values are the same, do nothing.
loadedHeadersBundle, ok := rawHeaderBundle.(*requestHeaderBundle)
if !ok {
return true, currentHeadersBundle, nil
}
if !equality.Semantic.DeepEqual(loadedHeadersBundle, currentHeadersBundle) {
return true, currentHeadersBundle, nil
}
return false, nil, nil
}
func (c *RequestHeaderAuthRequestController) getRequestHeaderBundleFromConfigMap(cm *corev1.ConfigMap) (*requestHeaderBundle, error) {
usernameHeaderCurrentValue, err := deserializeStrings(cm.Data[c.usernameHeadersKey])
if err != nil {
return nil, err
}
uidHeaderCurrentValue, err := deserializeStrings(cm.Data[c.uidHeadersKey])
if err != nil {
return nil, err
}
groupHeadersCurrentValue, err := deserializeStrings(cm.Data[c.groupHeadersKey])
if err != nil {
return nil, err
}
extraHeaderPrefixesCurrentValue, err := deserializeStrings(cm.Data[c.extraHeaderPrefixesKey])
if err != nil {
return nil, err
}
allowedClientNamesCurrentValue, err := deserializeStrings(cm.Data[c.allowedClientNamesKey])
if err != nil {
return nil, err
}
return &requestHeaderBundle{
UsernameHeaders: usernameHeaderCurrentValue,
UIDHeaders: uidHeaderCurrentValue,
GroupHeaders: groupHeadersCurrentValue,
ExtraHeaderPrefixes: extraHeaderPrefixesCurrentValue,
AllowedClientNames: allowedClientNamesCurrentValue,
}, nil
}
func (c *RequestHeaderAuthRequestController) loadRequestHeaderFor(key string) []string {
rawHeaderBundle := c.exportedRequestHeaderBundle.Load()
if rawHeaderBundle == nil {
return nil // this can happen if we've been unable load data from the apiserver for some reason
}
headerBundle := rawHeaderBundle.(*requestHeaderBundle)
switch key {
case c.usernameHeadersKey:
return headerBundle.UsernameHeaders
case c.uidHeadersKey:
return headerBundle.UIDHeaders
case c.groupHeadersKey:
return headerBundle.GroupHeaders
case c.extraHeaderPrefixesKey:
return headerBundle.ExtraHeaderPrefixes
case c.allowedClientNamesKey:
return headerBundle.AllowedClientNames
default:
return nil
}
}
func (c *RequestHeaderAuthRequestController) keyFn() string {
// this format matches DeletionHandlingMetaNamespaceKeyFunc for our single key
return c.configmapNamespace + "/" + c.configmapName
}
func deserializeStrings(in string) ([]string, error) {
if len(in) == 0 {
return nil, nil
}
var ret []string
if err := json.Unmarshal([]byte(in), &ret); err != nil {
return nil, err
}
return ret, nil
}

View File

@ -1,71 +0,0 @@
/*
Copyright 2014 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 union
import (
"net/http"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/authentication/authenticator"
)
// unionAuthRequestHandler authenticates requests using a chain of authenticator.Requests
type unionAuthRequestHandler struct {
// Handlers is a chain of request authenticators to delegate to
Handlers []authenticator.Request
// FailOnError determines whether an error returns short-circuits the chain
FailOnError bool
}
// New returns a request authenticator that validates credentials using a chain of authenticator.Request objects.
// The entire chain is tried until one succeeds. If all fail, an aggregate error is returned.
func New(authRequestHandlers ...authenticator.Request) authenticator.Request {
if len(authRequestHandlers) == 1 {
return authRequestHandlers[0]
}
return &unionAuthRequestHandler{Handlers: authRequestHandlers, FailOnError: false}
}
// NewFailOnError returns a request authenticator that validates credentials using a chain of authenticator.Request objects.
// The first error short-circuits the chain.
func NewFailOnError(authRequestHandlers ...authenticator.Request) authenticator.Request {
if len(authRequestHandlers) == 1 {
return authRequestHandlers[0]
}
return &unionAuthRequestHandler{Handlers: authRequestHandlers, FailOnError: true}
}
// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects.
func (authHandler *unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
var errlist []error
for _, currAuthRequestHandler := range authHandler.Handlers {
resp, ok, err := currAuthRequestHandler.AuthenticateRequest(req)
if err != nil {
if authHandler.FailOnError {
return resp, ok, err
}
errlist = append(errlist, err)
continue
}
if ok {
return resp, ok, err
}
}
return nil, false, utilerrors.NewAggregate(errlist)
}

View File

@ -1,108 +0,0 @@
/*
Copyright 2017 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 websocket
import (
"encoding/base64"
"errors"
"net/http"
"net/textproto"
"strings"
"unicode/utf8"
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
"k8s.io/apiserver/pkg/authentication/authenticator"
)
const bearerProtocolPrefix = "base64url.bearer.authorization.k8s.io."
var protocolHeader = textproto.CanonicalMIMEHeaderKey("Sec-WebSocket-Protocol")
var errInvalidToken = errors.New("invalid bearer token")
// ProtocolAuthenticator allows a websocket connection to provide a bearer token as a subprotocol
// in the format "base64url.bearer.authorization.<base64url-without-padding(bearer-token)>"
type ProtocolAuthenticator struct {
// auth is the token authenticator to use to validate the token
auth authenticator.Token
}
func NewProtocolAuthenticator(auth authenticator.Token) *ProtocolAuthenticator {
return &ProtocolAuthenticator{auth}
}
func (a *ProtocolAuthenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
// Only accept websocket connections
if !wsstream.IsWebSocketRequest(req) {
return nil, false, nil
}
token := ""
sawTokenProtocol := false
filteredProtocols := []string{}
for _, protocolHeader := range req.Header[protocolHeader] {
for _, protocol := range strings.Split(protocolHeader, ",") {
protocol = strings.TrimSpace(protocol)
if !strings.HasPrefix(protocol, bearerProtocolPrefix) {
filteredProtocols = append(filteredProtocols, protocol)
continue
}
if sawTokenProtocol {
return nil, false, errors.New("multiple base64.bearer.authorization tokens specified")
}
sawTokenProtocol = true
encodedToken := strings.TrimPrefix(protocol, bearerProtocolPrefix)
decodedToken, err := base64.RawURLEncoding.DecodeString(encodedToken)
if err != nil {
return nil, false, errors.New("invalid base64.bearer.authorization token encoding")
}
if !utf8.Valid(decodedToken) {
return nil, false, errors.New("invalid base64.bearer.authorization token")
}
token = string(decodedToken)
}
}
// Must pass at least one other subprotocol so that we can remove the one containing the bearer token,
// and there is at least one to echo back to the client
if len(token) > 0 && len(filteredProtocols) == 0 {
return nil, false, errors.New("missing additional subprotocol")
}
if len(token) == 0 {
return nil, false, nil
}
resp, ok, err := a.auth.AuthenticateToken(req.Context(), token)
// on success, remove the protocol with the token
if ok {
// https://tools.ietf.org/html/rfc6455#section-11.3.4 indicates the Sec-WebSocket-Protocol header may appear multiple times
// in a request, and is logically the same as a single Sec-WebSocket-Protocol header field that contains all values
req.Header.Set(protocolHeader, strings.Join(filteredProtocols, ","))
}
// If the token authenticator didn't error, provide a default error
if !ok && err == nil {
err = errInvalidToken
}
return resp, ok, err
}

View File

@ -1,8 +0,0 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- sig-auth-certificates-approvers
reviewers:
- sig-auth-certificates-reviewers
labels:
- sig/auth

View File

@ -1,19 +0,0 @@
/*
Copyright 2014 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 x509 provides a request authenticator that validates and
// extracts user information from client certificates
package x509

View File

@ -1,71 +0,0 @@
/*
Copyright 2019 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 x509
import (
"crypto/x509"
"fmt"
"k8s.io/client-go/util/cert"
)
// StaticVerifierFn is a VerifyOptionFunc that always returns the same value. This allows verify options that cannot change.
func StaticVerifierFn(opts x509.VerifyOptions) VerifyOptionFunc {
return func() (x509.VerifyOptions, bool) {
return opts, true
}
}
// NewStaticVerifierFromFile creates a new verification func from a file. It reads the content and then fails.
// It will return a nil function if you pass an empty CA file.
func NewStaticVerifierFromFile(clientCA string) (VerifyOptionFunc, error) {
if len(clientCA) == 0 {
return nil, nil
}
// Wrap with an x509 verifier
var err error
opts := DefaultVerifyOptions()
opts.Roots, err = cert.NewPool(clientCA)
if err != nil {
return nil, fmt.Errorf("error loading certs from %s: %v", clientCA, err)
}
return StaticVerifierFn(opts), nil
}
// StringSliceProvider is a way to get a string slice value. It is heavily used for authentication headers among other places.
type StringSliceProvider interface {
// Value returns the current string slice. Callers should never mutate the returned value.
Value() []string
}
// StringSliceProviderFunc is a function that matches the StringSliceProvider interface
type StringSliceProviderFunc func() []string
// Value returns the current string slice. Callers should never mutate the returned value.
func (d StringSliceProviderFunc) Value() []string {
return d()
}
// StaticStringSlice a StringSliceProvider that returns a fixed value
type StaticStringSlice []string
// Value returns the current string slice. Callers should never mutate the returned value.
func (s StaticStringSlice) Value() []string {
return s
}

View File

@ -1,332 +0,0 @@
/*
Copyright 2014 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 x509
import (
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strings"
"time"
asn1util "k8s.io/apimachinery/pkg/apis/asn1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
/*
* By default, the following metric is defined as falling under
* ALPHA stability level https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/1209-metrics-stability/kubernetes-control-plane-metrics-stability.md#stability-classes)
*
* Promoting the stability level of the metric is a responsibility of the component owner, since it
* involves explicitly acknowledging support for the metric across multiple releases, in accordance with
* the metric stability policy.
*/
var clientCertificateExpirationHistogram = metrics.NewHistogram(
&metrics.HistogramOpts{
Namespace: "apiserver",
Subsystem: "client",
Name: "certificate_expiration_seconds",
Help: "Distribution of the remaining lifetime on the certificate used to authenticate a request.",
Buckets: []float64{
0,
1800, // 30 minutes
3600, // 1 hour
7200, // 2 hours
21600, // 6 hours
43200, // 12 hours
86400, // 1 day
172800, // 2 days
345600, // 4 days
604800, // 1 week
2592000, // 1 month
7776000, // 3 months
15552000, // 6 months
31104000, // 1 year
},
StabilityLevel: metrics.ALPHA,
},
)
func init() {
legacyregistry.MustRegister(clientCertificateExpirationHistogram)
}
// UserConversion defines an interface for extracting user info from a client certificate chain
type UserConversion interface {
User(chain []*x509.Certificate) (*authenticator.Response, bool, error)
}
// UserConversionFunc is a function that implements the UserConversion interface.
type UserConversionFunc func(chain []*x509.Certificate) (*authenticator.Response, bool, error)
// User implements x509.UserConversion
func (f UserConversionFunc) User(chain []*x509.Certificate) (*authenticator.Response, bool, error) {
return f(chain)
}
func columnSeparatedHex(d []byte) string {
h := strings.ToUpper(hex.EncodeToString(d))
var sb strings.Builder
for i, r := range h {
sb.WriteRune(r)
if i%2 == 1 && i != len(h)-1 {
sb.WriteRune(':')
}
}
return sb.String()
}
func certificateIdentifier(c *x509.Certificate) string {
return fmt.Sprintf(
"SN=%d, SKID=%s, AKID=%s",
c.SerialNumber,
columnSeparatedHex(c.SubjectKeyId),
columnSeparatedHex(c.AuthorityKeyId),
)
}
// VerifyOptionFunc is function which provides a shallow copy of the VerifyOptions to the authenticator. This allows
// for cases where the options (particularly the CAs) can change. If the bool is false, then the returned VerifyOptions
// are ignored and the authenticator will express "no opinion". This allows a clear signal for cases where a CertPool
// is eventually expected, but not currently present.
type VerifyOptionFunc func() (x509.VerifyOptions, bool)
// Authenticator implements request.Authenticator by extracting user info from verified client certificates
type Authenticator struct {
verifyOptionsFn VerifyOptionFunc
user UserConversion
}
// New returns a request.Authenticator that verifies client certificates using the provided
// VerifyOptions, and converts valid certificate chains into user.Info using the provided UserConversion
func New(opts x509.VerifyOptions, user UserConversion) *Authenticator {
return NewDynamic(StaticVerifierFn(opts), user)
}
// NewDynamic returns a request.Authenticator that verifies client certificates using the provided
// VerifyOptionFunc (which may be dynamic), and converts valid certificate chains into user.Info using the provided UserConversion
func NewDynamic(verifyOptionsFn VerifyOptionFunc, user UserConversion) *Authenticator {
return &Authenticator{verifyOptionsFn, user}
}
// AuthenticateRequest authenticates the request using presented client certificates
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {
return nil, false, nil
}
// Use intermediates, if provided
optsCopy, ok := a.verifyOptionsFn()
// if there are intentionally no verify options, then we cannot authenticate this request
if !ok {
return nil, false, nil
}
if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 {
optsCopy.Intermediates = x509.NewCertPool()
for _, intermediate := range req.TLS.PeerCertificates[1:] {
optsCopy.Intermediates.AddCert(intermediate)
}
}
/*
kubernetes mutual (2-way) x509 between client and apiserver:
1. apiserver sending its apiserver certificate along with its publickey to client
2. client verifies the apiserver certificate sent against its cluster certificate authority data
3. client sending its client certificate along with its public key to the apiserver
>4. apiserver verifies the client certificate sent against its cluster certificate authority data
description:
here, with this function,
client certificate and pub key sent during the handshake process
are verified by apiserver against its cluster certificate authority data
normal args related to this stage:
--client-ca-file string If set, any request presenting a client certificate signed by
one of the authorities in the client-ca-file is authenticated with an identity
corresponding to the CommonName of the client certificate.
(retrievable from "kube-apiserver --help" command)
(suggested by @deads2k)
see also:
- for the step 1, see: staging/src/k8s.io/apiserver/pkg/server/options/serving.go
- for the step 2, see: staging/src/k8s.io/client-go/transport/transport.go
- for the step 3, see: staging/src/k8s.io/client-go/transport/transport.go
*/
remaining := req.TLS.PeerCertificates[0].NotAfter.Sub(time.Now())
clientCertificateExpirationHistogram.WithContext(req.Context()).Observe(remaining.Seconds())
chains, err := req.TLS.PeerCertificates[0].Verify(optsCopy)
if err != nil {
return nil, false, fmt.Errorf(
"verifying certificate %s failed: %w",
certificateIdentifier(req.TLS.PeerCertificates[0]),
err,
)
}
var errlist []error
for _, chain := range chains {
user, ok, err := a.user.User(chain)
if err != nil {
errlist = append(errlist, err)
continue
}
if ok {
return user, ok, err
}
}
return nil, false, utilerrors.NewAggregate(errlist)
}
// Verifier implements request.Authenticator by verifying a client cert on the request, then delegating to the wrapped auth
type Verifier struct {
verifyOptionsFn VerifyOptionFunc
auth authenticator.Request
// allowedCommonNames contains the common names which a verified certificate is allowed to have.
// If empty, all verified certificates are allowed.
allowedCommonNames StringSliceProvider
}
// NewVerifier create a request.Authenticator by verifying a client cert on the request, then delegating to the wrapped auth
func NewVerifier(opts x509.VerifyOptions, auth authenticator.Request, allowedCommonNames sets.String) authenticator.Request {
return NewDynamicCAVerifier(StaticVerifierFn(opts), auth, StaticStringSlice(allowedCommonNames.List()))
}
// NewDynamicCAVerifier create a request.Authenticator by verifying a client cert on the request, then delegating to the wrapped auth
func NewDynamicCAVerifier(verifyOptionsFn VerifyOptionFunc, auth authenticator.Request, allowedCommonNames StringSliceProvider) authenticator.Request {
return &Verifier{verifyOptionsFn, auth, allowedCommonNames}
}
// AuthenticateRequest verifies the presented client certificate, then delegates to the wrapped auth
func (a *Verifier) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {
return nil, false, nil
}
// Use intermediates, if provided
optsCopy, ok := a.verifyOptionsFn()
// if there are intentionally no verify options, then we cannot authenticate this request
if !ok {
return nil, false, nil
}
if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 {
optsCopy.Intermediates = x509.NewCertPool()
for _, intermediate := range req.TLS.PeerCertificates[1:] {
optsCopy.Intermediates.AddCert(intermediate)
}
}
if _, err := req.TLS.PeerCertificates[0].Verify(optsCopy); err != nil {
return nil, false, err
}
if err := a.verifySubject(req.TLS.PeerCertificates[0].Subject); err != nil {
return nil, false, err
}
return a.auth.AuthenticateRequest(req)
}
func (a *Verifier) verifySubject(subject pkix.Name) error {
// No CN restrictions
if len(a.allowedCommonNames.Value()) == 0 {
return nil
}
// Enforce CN restrictions
for _, allowedCommonName := range a.allowedCommonNames.Value() {
if allowedCommonName == subject.CommonName {
return nil
}
}
return fmt.Errorf("x509: subject with cn=%s is not in the allowed list", subject.CommonName)
}
// DefaultVerifyOptions returns VerifyOptions that use the system root certificates, current time,
// and requires certificates to be valid for client auth (x509.ExtKeyUsageClientAuth)
func DefaultVerifyOptions() x509.VerifyOptions {
return x509.VerifyOptions{
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
}
// CommonNameUserConversion builds user info from a certificate chain using the subject's CommonName
var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (*authenticator.Response, bool, error) {
if len(chain[0].Subject.CommonName) == 0 {
return nil, false, nil
}
fp := sha256.Sum256(chain[0].Raw)
id := "X509SHA256=" + hex.EncodeToString(fp[:])
uid, err := parseUIDFromCert(chain[0])
if err != nil {
return nil, false, err
}
return &authenticator.Response{
User: &user.DefaultInfo{
Name: chain[0].Subject.CommonName,
UID: uid,
Groups: chain[0].Subject.Organization,
Extra: map[string][]string{
user.CredentialIDKey: {id},
},
},
}, true, nil
})
var uidOID = asn1util.X509UID()
func parseUIDFromCert(cert *x509.Certificate) (string, error) {
if !utilfeature.DefaultFeatureGate.Enabled(features.AllowParsingUserUIDFromCertAuth) {
return "", nil
}
uids := []string{}
for _, name := range cert.Subject.Names {
if !name.Type.Equal(uidOID) {
continue
}
uid, ok := name.Value.(string)
if !ok {
return "", fmt.Errorf("unable to parse UID into a string")
}
uids = append(uids, uid)
}
if len(uids) == 0 {
return "", nil
}
if len(uids) != 1 {
return "", fmt.Errorf("expected 1 UID, but found multiple: %v", uids)
}
if uids[0] == "" {
return "", errors.New("UID cannot be an empty string")
}
return uids[0], nil
}

View File

@ -1,49 +0,0 @@
/*
Copyright 2017 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 cache
import (
"time"
utilcache "k8s.io/apimachinery/pkg/util/cache"
"k8s.io/utils/clock"
)
type simpleCache struct {
cache *utilcache.Expiring
}
func newSimpleCache(clock clock.Clock) cache {
return &simpleCache{cache: utilcache.NewExpiringWithClock(clock)}
}
func (c *simpleCache) get(key string) (*cacheRecord, bool) {
record, ok := c.cache.Get(key)
if !ok {
return nil, false
}
value, ok := record.(*cacheRecord)
return value, ok
}
func (c *simpleCache) set(key string, value *cacheRecord, ttl time.Duration) {
c.cache.Set(key, value, ttl)
}
func (c *simpleCache) remove(key string) {
c.cache.Delete(key)
}

View File

@ -1,60 +0,0 @@
/*
Copyright 2017 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 cache
import (
"hash/fnv"
"time"
)
// split cache lookups across N striped caches
type stripedCache struct {
stripeCount uint32
hashFunc func(string) uint32
caches []cache
}
type hashFunc func(string) uint32
type newCacheFunc func() cache
func newStripedCache(stripeCount int, hash hashFunc, newCacheFunc newCacheFunc) cache {
caches := []cache{}
for i := 0; i < stripeCount; i++ {
caches = append(caches, newCacheFunc())
}
return &stripedCache{
stripeCount: uint32(stripeCount),
hashFunc: hash,
caches: caches,
}
}
func (c *stripedCache) get(key string) (*cacheRecord, bool) {
return c.caches[c.hashFunc(key)%c.stripeCount].get(key)
}
func (c *stripedCache) set(key string, value *cacheRecord, ttl time.Duration) {
c.caches[c.hashFunc(key)%c.stripeCount].set(key, value, ttl)
}
func (c *stripedCache) remove(key string) {
c.caches[c.hashFunc(key)%c.stripeCount].remove(key)
}
func fnvHashFunc(key string) uint32 {
f := fnv.New32()
f.Write([]byte(key))
return f.Sum32()
}

View File

@ -1,318 +0,0 @@
/*
Copyright 2017 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 cache
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"hash"
"io"
"runtime"
"sync"
"time"
"unsafe"
"golang.org/x/sync/singleflight"
apierrors "k8s.io/apimachinery/pkg/api/errors"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/warning"
"k8s.io/klog/v2"
"k8s.io/utils/clock"
)
var errAuthnCrash = apierrors.NewInternalError(errors.New("authentication failed unexpectedly"))
const sharedLookupTimeout = 30 * time.Second
// cacheRecord holds the three return values of the authenticator.Token AuthenticateToken method
type cacheRecord struct {
resp *authenticator.Response
ok bool
err error
// this cache assumes token authn has no side-effects or temporal dependence.
// neither of these are true for audit annotations set via AddAuditAnnotation.
//
// for audit annotations, the assumption is that for some period of time (cache TTL),
// all requests with the same API audiences and the same bearer token result in the
// same annotations. This may not be true if the authenticator sets an annotation
// based on the current time, but that may be okay since cache TTLs are generally
// small (seconds).
annotations map[string]string
warnings []*cacheWarning
}
type cacheWarning struct {
agent string
text string
}
type cachedTokenAuthenticator struct {
authenticator authenticator.Token
cacheErrs bool
successTTL time.Duration
failureTTL time.Duration
cache cache
group singleflight.Group
// hashPool is a per authenticator pool of hash.Hash (to avoid allocations from building the Hash)
// HMAC with SHA-256 and a random key is used to prevent precomputation and length extension attacks
// It also mitigates hash map DOS attacks via collisions (the inputs are supplied by untrusted users)
hashPool *sync.Pool
}
type cache interface {
// given a key, return the record, and whether or not it existed
get(key string) (value *cacheRecord, exists bool)
// caches the record for the key
set(key string, value *cacheRecord, ttl time.Duration)
// removes the record for the key
remove(key string)
}
// New returns a token authenticator that caches the results of the specified authenticator. A ttl of 0 bypasses the cache.
func New(authenticator authenticator.Token, cacheErrs bool, successTTL, failureTTL time.Duration) authenticator.Token {
return newWithClock(authenticator, cacheErrs, successTTL, failureTTL, clock.RealClock{})
}
func newWithClock(authenticator authenticator.Token, cacheErrs bool, successTTL, failureTTL time.Duration, clock clock.Clock) authenticator.Token {
randomCacheKey := make([]byte, 32)
if _, err := rand.Read(randomCacheKey); err != nil {
panic(err) // rand should never fail
}
return &cachedTokenAuthenticator{
authenticator: authenticator,
cacheErrs: cacheErrs,
successTTL: successTTL,
failureTTL: failureTTL,
// Cache performance degrades noticeably when the number of
// tokens in operation exceeds the size of the cache. It is
// cheap to make the cache big in the second dimension below,
// the memory is only consumed when that many tokens are being
// used. Currently we advertise support 5k nodes and 10k
// namespaces; a 32k entry cache is therefore a 2x safety
// margin.
cache: newStripedCache(32, fnvHashFunc, func() cache { return newSimpleCache(clock) }),
hashPool: &sync.Pool{
New: func() interface{} {
return hmac.New(sha256.New, randomCacheKey)
},
},
}
}
// AuthenticateToken implements authenticator.Token
func (a *cachedTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
record := a.doAuthenticateToken(ctx, token)
if !record.ok || record.err != nil {
return nil, false, record.err
}
for key, value := range record.annotations {
audit.AddAuditAnnotation(ctx, key, value)
}
for _, w := range record.warnings {
warning.AddWarning(ctx, w.agent, w.text)
}
return record.resp, true, nil
}
func (a *cachedTokenAuthenticator) doAuthenticateToken(ctx context.Context, token string) *cacheRecord {
doneAuthenticating := stats.authenticating(ctx)
auds, audsOk := authenticator.AudiencesFrom(ctx)
key := keyFunc(a.hashPool, auds, token)
if record, ok := a.cache.get(key); ok {
// Record cache hit
doneAuthenticating(true)
return record
}
// Record cache miss
doneBlocking := stats.blocking(ctx)
defer doneBlocking()
defer doneAuthenticating(false)
c := a.group.DoChan(key, func() (val interface{}, _ error) {
// always use one place to read and write the output of AuthenticateToken
record := &cacheRecord{}
doneFetching := stats.fetching(ctx)
// We're leaving the request handling stack so we need to handle crashes
// ourselves. Log a stack trace and return a 500 if something panics.
defer func() {
if r := recover(); r != nil {
// make sure to always return a record
record.err = errAuthnCrash
val = record
// Same as stdlib http server code. Manually allocate stack
// trace buffer size to prevent excessively large logs
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
klog.Errorf("%v\n%s", r, buf)
}
doneFetching(record.err == nil)
}()
// Check again for a cached record. We may have raced with a fetch.
if record, ok := a.cache.get(key); ok {
return record, nil
}
// Detach the context because the lookup may be shared by multiple callers,
// however propagate the audience.
ctx, cancel := context.WithTimeout(context.Background(), sharedLookupTimeout)
defer cancel()
if audsOk {
ctx = authenticator.WithAudiences(ctx, auds)
}
recorder := &recorder{}
ctx = warning.WithWarningRecorder(ctx, recorder)
ctx = audit.WithAuditContext(ctx)
ac := audit.AuditContextFrom(ctx)
// since this is shared work between multiple requests, we have no way of knowing if any
// particular request supports audit annotations. thus we always attempt to record them.
ac.Event.Level = auditinternal.LevelMetadata
record.resp, record.ok, record.err = a.authenticator.AuthenticateToken(ctx, token)
record.annotations = ac.Event.Annotations
record.warnings = recorder.extractWarnings()
if !a.cacheErrs && record.err != nil {
return record, nil
}
switch {
case record.ok && a.successTTL > 0:
a.cache.set(key, record, a.successTTL)
case !record.ok && a.failureTTL > 0:
a.cache.set(key, record, a.failureTTL)
}
return record, nil
})
select {
case result := <-c:
// we always set Val and never set Err
return result.Val.(*cacheRecord)
case <-ctx.Done():
// fake a record on context cancel
return &cacheRecord{err: ctx.Err()}
}
}
// keyFunc generates a string key by hashing the inputs.
// This lowers the memory requirement of the cache and keeps tokens out of memory.
func keyFunc(hashPool *sync.Pool, auds []string, token string) string {
h := hashPool.Get().(hash.Hash)
h.Reset()
// try to force stack allocation
var a [4]byte
b := a[:]
writeLengthPrefixedString(h, b, token)
// encode the length of audiences to avoid ambiguities
writeLength(h, b, len(auds))
for _, aud := range auds {
writeLengthPrefixedString(h, b, aud)
}
key := toString(h.Sum(nil)) // skip base64 encoding to save an allocation
hashPool.Put(h)
return key
}
// writeLengthPrefixedString writes s with a length prefix to prevent ambiguities, i.e. "xy" + "z" == "x" + "yz"
// the length of b is assumed to be 4 (b is mutated by this function to store the length of s)
func writeLengthPrefixedString(w io.Writer, b []byte, s string) {
writeLength(w, b, len(s))
if _, err := w.Write(toBytes(s)); err != nil {
panic(err) // Write() on hash never fails
}
}
// writeLength encodes length into b and then writes it via the given writer
// the length of b is assumed to be 4
func writeLength(w io.Writer, b []byte, length int) {
binary.BigEndian.PutUint32(b, uint32(length))
if _, err := w.Write(b); err != nil {
panic(err) // Write() on hash never fails
}
}
// toBytes performs unholy acts to avoid allocations
func toBytes(s string) []byte {
// unsafe.StringData is unspecified for the empty string, so we provide a strict interpretation
if len(s) == 0 {
return nil
}
// Copied from go 1.20.1 os.File.WriteString
// https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/os/file.go#L246
return unsafe.Slice(unsafe.StringData(s), len(s))
}
// toString performs unholy acts to avoid allocations
func toString(b []byte) string {
// unsafe.SliceData relies on cap whereas we want to rely on len
if len(b) == 0 {
return ""
}
// Copied from go 1.20.1 strings.Builder.String
// https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/strings/builder.go#L48
return unsafe.String(unsafe.SliceData(b), len(b))
}
// simple recorder that only appends warning
type recorder struct {
mu sync.Mutex
warnings []*cacheWarning
}
// AddWarning adds a warning to recorder.
func (r *recorder) AddWarning(agent, text string) {
r.mu.Lock()
defer r.mu.Unlock()
r.warnings = append(r.warnings, &cacheWarning{agent: agent, text: text})
}
func (r *recorder) extractWarnings() []*cacheWarning {
r.mu.Lock()
defer r.mu.Unlock()
warnings := r.warnings
r.warnings = nil
return warnings
}

View File

@ -1,126 +0,0 @@
/*
Copyright 2019 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 cache
import (
"context"
"time"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
var (
requestLatency = metrics.NewHistogramVec(
&metrics.HistogramOpts{
Namespace: "authentication",
Subsystem: "token_cache",
Name: "request_duration_seconds",
StabilityLevel: metrics.ALPHA,
},
[]string{"status"},
)
requestCount = metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: "authentication",
Subsystem: "token_cache",
Name: "request_total",
StabilityLevel: metrics.ALPHA,
},
[]string{"status"},
)
fetchCount = metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: "authentication",
Subsystem: "token_cache",
Name: "fetch_total",
StabilityLevel: metrics.ALPHA,
},
[]string{"status"},
)
activeFetchCount = metrics.NewGaugeVec(
&metrics.GaugeOpts{
Namespace: "authentication",
Subsystem: "token_cache",
Name: "active_fetch_count",
StabilityLevel: metrics.ALPHA,
},
[]string{"status"},
)
)
func init() {
legacyregistry.MustRegister(
requestLatency,
requestCount,
fetchCount,
activeFetchCount,
)
}
const (
hitTag = "hit"
missTag = "miss"
fetchFailedTag = "error"
fetchOkTag = "ok"
fetchInFlightTag = "in_flight"
fetchBlockedTag = "blocked"
)
type statsCollector struct{}
var stats = statsCollector{}
func (statsCollector) authenticating(ctx context.Context) func(hit bool) {
start := time.Now()
return func(hit bool) {
var tag string
if hit {
tag = hitTag
} else {
tag = missTag
}
latency := time.Since(start)
requestCount.WithContext(ctx).WithLabelValues(tag).Inc()
requestLatency.WithContext(ctx).WithLabelValues(tag).Observe(float64(latency.Milliseconds()) / 1000)
}
}
func (statsCollector) blocking(ctx context.Context) func() {
activeFetchCount.WithContext(ctx).WithLabelValues(fetchBlockedTag).Inc()
return activeFetchCount.WithContext(ctx).WithLabelValues(fetchBlockedTag).Dec
}
func (statsCollector) fetching(ctx context.Context) func(ok bool) {
activeFetchCount.WithContext(ctx).WithLabelValues(fetchInFlightTag).Inc()
return func(ok bool) {
var tag string
if ok {
tag = fetchOkTag
} else {
tag = fetchFailedTag
}
fetchCount.WithContext(ctx).WithLabelValues(tag).Inc()
activeFetchCount.WithContext(ctx).WithLabelValues(fetchInFlightTag).Dec()
}
}

View File

@ -1,99 +0,0 @@
/*
Copyright 2014 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 tokenfile
import (
"context"
"encoding/csv"
"fmt"
"io"
"os"
"strings"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog/v2"
)
type TokenAuthenticator struct {
tokens map[string]*user.DefaultInfo
}
// New returns a TokenAuthenticator for a single token
func New(tokens map[string]*user.DefaultInfo) *TokenAuthenticator {
return &TokenAuthenticator{
tokens: tokens,
}
}
// NewCSV returns a TokenAuthenticator, populated from a CSV file.
// The CSV file must contain records in the format "token,username,useruid"
func NewCSV(path string) (*TokenAuthenticator, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
recordNum := 0
tokens := make(map[string]*user.DefaultInfo)
reader := csv.NewReader(file)
reader.FieldsPerRecord = -1
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if len(record) < 3 {
return nil, fmt.Errorf("token file '%s' must have at least 3 columns (token, user name, user uid), found %d", path, len(record))
}
recordNum++
if record[0] == "" {
klog.Warningf("empty token has been found in token file '%s', record number '%d'", path, recordNum)
continue
}
obj := &user.DefaultInfo{
Name: record[1],
UID: record[2],
}
if _, exist := tokens[record[0]]; exist {
klog.Warningf("duplicate token has been found in token file '%s', record number '%d'", path, recordNum)
}
tokens[record[0]] = obj
if len(record) >= 4 {
obj.Groups = strings.Split(record[3], ",")
}
}
return &TokenAuthenticator{
tokens: tokens,
}, nil
}
func (a *TokenAuthenticator) AuthenticateToken(ctx context.Context, value string) (*authenticator.Response, bool, error) {
user, ok := a.tokens[value]
if !ok {
return nil, false, nil
}
return &authenticator.Response{User: user}, true, nil
}

View File

@ -1,95 +0,0 @@
/*
Copyright 2016 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 authorizerfactory
import (
"context"
"errors"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// alwaysAllowAuthorizer is an implementation of authorizer.Attributes
// which always says yes to an authorization request.
// It is useful in tests and when using kubernetes in an open manner.
type alwaysAllowAuthorizer struct{}
func (alwaysAllowAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionAllow, "", nil
}
func (alwaysAllowAuthorizer) RulesFor(ctx context.Context, user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
return []authorizer.ResourceRuleInfo{
&authorizer.DefaultResourceRuleInfo{
Verbs: []string{"*"},
APIGroups: []string{"*"},
Resources: []string{"*"},
},
}, []authorizer.NonResourceRuleInfo{
&authorizer.DefaultNonResourceRuleInfo{
Verbs: []string{"*"},
NonResourceURLs: []string{"*"},
},
}, false, nil
}
func NewAlwaysAllowAuthorizer() *alwaysAllowAuthorizer {
return new(alwaysAllowAuthorizer)
}
// alwaysDenyAuthorizer is an implementation of authorizer.Attributes
// which always says no to an authorization request.
// It is useful in unit tests to force an operation to be forbidden.
type alwaysDenyAuthorizer struct{}
func (alwaysDenyAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
return authorizer.DecisionNoOpinion, "Everything is forbidden.", nil
}
func (alwaysDenyAuthorizer) RulesFor(ctx context.Context, user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
return []authorizer.ResourceRuleInfo{}, []authorizer.NonResourceRuleInfo{}, false, nil
}
func NewAlwaysDenyAuthorizer() *alwaysDenyAuthorizer {
return new(alwaysDenyAuthorizer)
}
type privilegedGroupAuthorizer struct {
groups []string
}
func (r *privilegedGroupAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
if attr.GetUser() == nil {
return authorizer.DecisionNoOpinion, "Error", errors.New("no user on request.")
}
for _, attr_group := range attr.GetUser().GetGroups() {
for _, priv_group := range r.groups {
if priv_group == attr_group {
return authorizer.DecisionAllow, "", nil
}
}
}
return authorizer.DecisionNoOpinion, "", nil
}
// NewPrivilegedGroups is for use in loopback scenarios
func NewPrivilegedGroups(groups ...string) *privilegedGroupAuthorizer {
return &privilegedGroupAuthorizer{
groups: groups,
}
}

View File

@ -1,69 +0,0 @@
/*
Copyright 2016 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 authorizerfactory
import (
"errors"
"time"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authorization/authorizer"
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
)
// DelegatingAuthorizerConfig is the minimal configuration needed to create an authorizer
// built to delegate authorization to a kube API server
type DelegatingAuthorizerConfig struct {
SubjectAccessReviewClient authorizationclient.AuthorizationV1Interface
// Compiler is the CEL compiler to use for evaluating policies. If nil, a default compiler will be used.
Compiler authorizationcel.Compiler
// AllowCacheTTL is the length of time that a successful authorization response will be cached
AllowCacheTTL time.Duration
// DenyCacheTTL is the length of time that an unsuccessful authorization response will be cached.
// You generally want more responsive, "deny, try again" flows.
DenyCacheTTL time.Duration
// WebhookRetryBackoff specifies the backoff parameters for the authorization webhook retry logic.
// This allows us to configure the sleep time at each iteration and the maximum number of retries allowed
// before we fail the webhook call in order to limit the fan out that ensues when the system is degraded.
WebhookRetryBackoff *wait.Backoff
}
func (c DelegatingAuthorizerConfig) New() (authorizer.Authorizer, error) {
if c.WebhookRetryBackoff == nil {
return nil, errors.New("retry backoff parameters for delegating authorization webhook has not been specified")
}
compiler := c.Compiler
if compiler == nil {
compiler = authorizationcel.NewDefaultCompiler()
}
return webhook.NewFromInterface(
c.SubjectAccessReviewClient,
c.AllowCacheTTL,
c.DenyCacheTTL,
*c.WebhookRetryBackoff,
authorizer.DecisionNoOpinion,
NewDelegatingAuthorizerMetrics(),
compiler,
)
}

View File

@ -1,82 +0,0 @@
/*
Copyright 2021 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 authorizerfactory
import (
"context"
"sync"
celmetrics "k8s.io/apiserver/pkg/authorization/cel"
webhookmetrics "k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
compbasemetrics "k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
var registerMetrics sync.Once
// RegisterMetrics registers authorizer metrics.
func RegisterMetrics() {
registerMetrics.Do(func() {
legacyregistry.MustRegister(requestTotal)
legacyregistry.MustRegister(requestLatency)
})
}
var (
requestTotal = compbasemetrics.NewCounterVec(
&compbasemetrics.CounterOpts{
Name: "apiserver_delegated_authz_request_total",
Help: "Number of HTTP requests partitioned by status code.",
StabilityLevel: compbasemetrics.ALPHA,
},
[]string{"code"},
)
requestLatency = compbasemetrics.NewHistogramVec(
&compbasemetrics.HistogramOpts{
Name: "apiserver_delegated_authz_request_duration_seconds",
Help: "Request latency in seconds. Broken down by status code.",
Buckets: []float64{0.25, 0.5, 0.7, 1, 1.5, 3, 5, 10},
StabilityLevel: compbasemetrics.ALPHA,
},
[]string{"code"},
)
)
var _ = webhookmetrics.AuthorizerMetrics(delegatingAuthorizerMetrics{})
type delegatingAuthorizerMetrics struct {
// no-op for webhook metrics for now, delegating authorization reports original total/latency metrics
webhookmetrics.NoopWebhookMetrics
// no-op for matchCondition metrics for now, delegating authorization doesn't configure match conditions
celmetrics.NoopMatcherMetrics
}
func NewDelegatingAuthorizerMetrics() delegatingAuthorizerMetrics {
RegisterMetrics()
return delegatingAuthorizerMetrics{}
}
// RecordRequestTotal increments the total number of requests for the delegated authorization.
func (delegatingAuthorizerMetrics) RecordRequestTotal(ctx context.Context, code string) {
requestTotal.WithContext(ctx).WithLabelValues(code).Add(1)
}
// RecordRequestLatency measures request latency in seconds for the delegated authorization. Broken down by status code.
func (delegatingAuthorizerMetrics) RecordRequestLatency(ctx context.Context, code string, latency float64) {
requestLatency.WithContext(ctx).WithLabelValues(code).Observe(latency)
}

View File

@ -1,336 +0,0 @@
/*
Copyright 2023 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 cel
import (
"fmt"
"github.com/google/cel-go/cel"
celast "github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/types/ref"
authorizationv1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/util/version"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
const (
subjectAccessReviewRequestVarName = "request"
fieldSelectorVarName = "fieldSelector"
labelSelectorVarName = "labelSelector"
)
// CompilationResult represents a compiled authorization cel expression.
type CompilationResult struct {
Program cel.Program
ExpressionAccessor ExpressionAccessor
// These track if a given expression uses fieldSelector and labelSelector,
// so construction of data passed to the CEL expression can be optimized if those fields are unused.
UsesFieldSelector bool
UsesLabelSelector bool
}
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
type EvaluationResult struct {
EvalResult ref.Val
ExpressionAccessor ExpressionAccessor
}
// Compiler is an interface for compiling CEL expressions with the desired environment mode.
type Compiler interface {
CompileCELExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error)
}
type compiler struct {
envSet *environment.EnvSet
}
// NewDefaultCompiler returns a new Compiler following the default compatibility version.
// Note: the compiler construction depends on feature gates and the compatibility version to be initialized.
func NewDefaultCompiler() Compiler {
return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
}
// NewCompiler returns a new Compiler.
func NewCompiler(env *environment.EnvSet) Compiler {
return &compiler{
envSet: mustBuildEnv(env),
}
}
func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error) {
resultError := func(errorString string, errType apiservercel.ErrorType) (CompilationResult, error) {
err := &apiservercel.Error{
Type: errType,
Detail: errorString,
}
return CompilationResult{
ExpressionAccessor: expressionAccessor,
}, err
}
env, err := c.envSet.Env(environment.StoredExpressions)
if err != nil {
return resultError(fmt.Sprintf("unexpected error loading CEL environment: %v", err), apiservercel.ErrorTypeInternal)
}
ast, issues := env.Compile(expressionAccessor.GetExpression())
if issues != nil {
return resultError("compilation failed: "+issues.String(), apiservercel.ErrorTypeInvalid)
}
found := false
returnTypes := expressionAccessor.ReturnTypes()
for _, returnType := range returnTypes {
if ast.OutputType() == returnType {
found = true
break
}
}
if !found {
var reason string
if len(returnTypes) == 1 {
reason = fmt.Sprintf("must evaluate to %v but got %v", returnTypes[0].String(), ast.OutputType())
} else {
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
}
return resultError(reason, apiservercel.ErrorTypeInvalid)
}
checkedExpr, err := cel.AstToCheckedExpr(ast)
if err != nil {
// should be impossible since env.Compile returned no issues
return resultError("unexpected compilation error: "+err.Error(), apiservercel.ErrorTypeInternal)
}
celAST, err := celast.ToAST(checkedExpr)
if err != nil {
// should be impossible since env.Compile returned no issues
return resultError("unexpected compilation error: "+err.Error(), apiservercel.ErrorTypeInternal)
}
var usesFieldSelector, usesLabelSelector bool
celast.PreOrderVisit(celast.NavigateAST(celAST), celast.NewExprVisitor(func(e celast.Expr) {
// we already know we use both, no need to inspect more
if usesFieldSelector && usesLabelSelector {
return
}
var fieldName string
switch e.Kind() {
case celast.SelectKind:
// simple select (.fieldSelector / .labelSelector)
fieldName = e.AsSelect().FieldName()
case celast.CallKind:
// optional select (.?fieldSelector / .?labelSelector)
if e.AsCall().FunctionName() != operators.OptSelect {
return
}
args := e.AsCall().Args()
// args[0] is the receiver (what comes before the `.?`), args[1] is the field name being optionally selected (what comes after the `.?`)
if len(args) != 2 || args[1].Kind() != celast.LiteralKind || args[1].AsLiteral().Type() != cel.StringType {
return
}
fieldName, _ = args[1].AsLiteral().Value().(string)
}
switch fieldName {
case fieldSelectorVarName:
usesFieldSelector = true
case labelSelectorVarName:
usesLabelSelector = true
}
}))
prog, err := env.Program(ast)
if err != nil {
return resultError("program instantiation failed: "+err.Error(), apiservercel.ErrorTypeInternal)
}
return CompilationResult{
Program: prog,
ExpressionAccessor: expressionAccessor,
UsesFieldSelector: usesFieldSelector,
UsesLabelSelector: usesLabelSelector,
}, nil
}
func mustBuildEnv(baseEnv *environment.EnvSet) *environment.EnvSet {
field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
return apiservercel.NewDeclField(name, declType, required, nil, nil)
}
fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField {
result := make(map[string]*apiservercel.DeclField, len(fields))
for _, f := range fields {
result[f.Name] = f
}
return result
}
subjectAccessReviewSpecRequestType := buildRequestType(field, fields)
extended, err := baseEnv.Extend(
environment.VersionedOptions{
// we record this as 1.0 since it was available in the
// first version that supported this feature
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: []cel.EnvOption{
cel.Variable(subjectAccessReviewRequestVarName, subjectAccessReviewSpecRequestType.CelType()),
},
DeclTypes: []*apiservercel.DeclType{
subjectAccessReviewSpecRequestType,
},
},
)
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
}
return extended
}
// buildRequestType generates a DeclType for SubjectAccessReviewSpec.
// if attributes are added here, also add to convertObjectToUnstructured.
func buildRequestType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
resourceAttributesType := buildResourceAttributesType(field, fields)
nonResourceAttributesType := buildNonResourceAttributesType(field, fields)
return apiservercel.NewObjectType("kubernetes.SubjectAccessReviewSpec", fields(
field("resourceAttributes", resourceAttributesType, false),
field("nonResourceAttributes", nonResourceAttributesType, false),
field("user", apiservercel.StringType, false),
field("groups", apiservercel.NewListType(apiservercel.StringType, -1), false),
field("extra", apiservercel.NewMapType(apiservercel.StringType, apiservercel.NewListType(apiservercel.StringType, -1), -1), false),
field("uid", apiservercel.StringType, false),
))
}
// buildResourceAttributesType generates a DeclType for ResourceAttributes.
// if attributes are added here, also add to convertObjectToUnstructured.
func buildResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
resourceAttributesFields := []*apiservercel.DeclField{
field("namespace", apiservercel.StringType, false),
field("verb", apiservercel.StringType, false),
field("group", apiservercel.StringType, false),
field("version", apiservercel.StringType, false),
field("resource", apiservercel.StringType, false),
field("subresource", apiservercel.StringType, false),
field("name", apiservercel.StringType, false),
}
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
resourceAttributesFields = append(resourceAttributesFields, field("fieldSelector", buildFieldSelectorType(field, fields), false))
resourceAttributesFields = append(resourceAttributesFields, field("labelSelector", buildLabelSelectorType(field, fields), false))
}
return apiservercel.NewObjectType("kubernetes.ResourceAttributes", fields(resourceAttributesFields...))
}
func buildFieldSelectorType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
return apiservercel.NewObjectType("kubernetes.FieldSelectorAttributes", fields(
field("rawSelector", apiservercel.StringType, false),
field("requirements", apiservercel.NewListType(buildSelectorRequirementType(field, fields), -1), false),
))
}
func buildLabelSelectorType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
return apiservercel.NewObjectType("kubernetes.LabelSelectorAttributes", fields(
field("rawSelector", apiservercel.StringType, false),
field("requirements", apiservercel.NewListType(buildSelectorRequirementType(field, fields), -1), false),
))
}
func buildSelectorRequirementType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
return apiservercel.NewObjectType("kubernetes.SelectorRequirement", fields(
field("key", apiservercel.StringType, false),
field("operator", apiservercel.StringType, false),
field("values", apiservercel.NewListType(apiservercel.StringType, -1), false),
))
}
// buildNonResourceAttributesType generates a DeclType for NonResourceAttributes.
// if attributes are added here, also add to convertObjectToUnstructured.
func buildNonResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
return apiservercel.NewObjectType("kubernetes.NonResourceAttributes", fields(
field("path", apiservercel.StringType, false),
field("verb", apiservercel.StringType, false),
))
}
func convertObjectToUnstructured(obj *authorizationv1.SubjectAccessReviewSpec, includeFieldSelector, includeLabelSelector bool) map[string]interface{} {
// Construct version containing every SubjectAccessReview user and string attribute field, even omitempty ones, for evaluation by CEL
extra := obj.Extra
if extra == nil {
extra = map[string]authorizationv1.ExtraValue{}
}
ret := map[string]interface{}{
"user": obj.User,
"groups": obj.Groups,
"uid": string(obj.UID),
"extra": extra,
}
if obj.ResourceAttributes != nil {
resourceAttributes := map[string]interface{}{
"namespace": obj.ResourceAttributes.Namespace,
"verb": obj.ResourceAttributes.Verb,
"group": obj.ResourceAttributes.Group,
"version": obj.ResourceAttributes.Version,
"resource": obj.ResourceAttributes.Resource,
"subresource": obj.ResourceAttributes.Subresource,
"name": obj.ResourceAttributes.Name,
}
if includeFieldSelector && obj.ResourceAttributes.FieldSelector != nil {
if len(obj.ResourceAttributes.FieldSelector.Requirements) > 0 {
requirements := make([]map[string]interface{}, 0, len(obj.ResourceAttributes.FieldSelector.Requirements))
for _, r := range obj.ResourceAttributes.FieldSelector.Requirements {
requirements = append(requirements, map[string]interface{}{
"key": r.Key,
"operator": r.Operator,
"values": r.Values,
})
}
resourceAttributes[fieldSelectorVarName] = map[string]interface{}{"requirements": requirements}
}
if len(obj.ResourceAttributes.FieldSelector.RawSelector) > 0 {
resourceAttributes[fieldSelectorVarName] = map[string]interface{}{"rawSelector": obj.ResourceAttributes.FieldSelector.RawSelector}
}
}
if includeLabelSelector && obj.ResourceAttributes.LabelSelector != nil {
if len(obj.ResourceAttributes.LabelSelector.Requirements) > 0 {
requirements := make([]map[string]interface{}, 0, len(obj.ResourceAttributes.LabelSelector.Requirements))
for _, r := range obj.ResourceAttributes.LabelSelector.Requirements {
requirements = append(requirements, map[string]interface{}{
"key": r.Key,
"operator": r.Operator,
"values": r.Values,
})
}
resourceAttributes[labelSelectorVarName] = map[string]interface{}{"requirements": requirements}
}
if len(obj.ResourceAttributes.LabelSelector.RawSelector) > 0 {
resourceAttributes[labelSelectorVarName] = map[string]interface{}{"rawSelector": obj.ResourceAttributes.LabelSelector.RawSelector}
}
}
ret["resourceAttributes"] = resourceAttributes
}
if obj.NonResourceAttributes != nil {
ret["nonResourceAttributes"] = map[string]string{
"verb": obj.NonResourceAttributes.Verb,
"path": obj.NonResourceAttributes.Path,
}
}
return ret
}

View File

@ -1,41 +0,0 @@
/*
Copyright 2023 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 cel
import (
celgo "github.com/google/cel-go/cel"
)
type ExpressionAccessor interface {
GetExpression() string
ReturnTypes() []*celgo.Type
}
var _ ExpressionAccessor = &SubjectAccessReviewMatchCondition{}
// SubjectAccessReviewMatchCondition is a CEL expression that maps a SubjectAccessReview request to a list of values.
type SubjectAccessReviewMatchCondition struct {
Expression string
}
func (v *SubjectAccessReviewMatchCondition) GetExpression() string {
return v.Expression
}
func (v *SubjectAccessReviewMatchCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}

View File

@ -1,91 +0,0 @@
/*
Copyright 2023 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 cel
import (
"context"
"fmt"
"time"
celgo "github.com/google/cel-go/cel"
authorizationv1 "k8s.io/api/authorization/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
)
type CELMatcher struct {
CompilationResults []CompilationResult
// These track if any expressions use fieldSelector and labelSelector,
// so construction of data passed to the CEL expression can be optimized if those fields are unused.
UsesLabelSelector bool
UsesFieldSelector bool
// These are optional fields which can be populated if metrics reporting is desired
Metrics MatcherMetrics
AuthorizerType string
AuthorizerName string
}
// eval evaluates the given SubjectAccessReview against all cel matchCondition expression
func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) {
var evalErrors []error
metrics := c.Metrics
if metrics == nil {
metrics = NoopMatcherMetrics{}
}
start := time.Now()
defer func() {
metrics.RecordAuthorizationMatchConditionEvaluation(ctx, c.AuthorizerType, c.AuthorizerName, time.Since(start))
if len(evalErrors) > 0 {
metrics.RecordAuthorizationMatchConditionEvaluationFailure(ctx, c.AuthorizerType, c.AuthorizerName)
}
}()
va := map[string]interface{}{
"request": convertObjectToUnstructured(&r.Spec, c.UsesFieldSelector, c.UsesLabelSelector),
}
for _, compilationResult := range c.CompilationResults {
evalResult, _, err := compilationResult.Program.ContextEval(ctx, va)
if err != nil {
evalErrors = append(evalErrors, fmt.Errorf("cel evaluation error: expression '%v' resulted in error: %w", compilationResult.ExpressionAccessor.GetExpression(), err))
continue
}
if evalResult.Type() != celgo.BoolType {
evalErrors = append(evalErrors, fmt.Errorf("cel evaluation error: expression '%v' eval result type should be bool but got %W", compilationResult.ExpressionAccessor.GetExpression(), evalResult.Type()))
continue
}
match, ok := evalResult.Value().(bool)
if !ok {
evalErrors = append(evalErrors, fmt.Errorf("cel evaluation error: expression '%v' eval result value should be bool but got %W", compilationResult.ExpressionAccessor.GetExpression(), evalResult.Value()))
continue
}
// If at least one matchCondition successfully evaluates to FALSE,
// return early
if !match {
metrics.RecordAuthorizationMatchConditionExclusion(ctx, c.AuthorizerType, c.AuthorizerName)
return false, nil
}
}
// if there is any error, return
if len(evalErrors) > 0 {
return false, utilerrors.NewAggregate(evalErrors)
}
// return ALL matchConditions evaluate to TRUE successfully without error
return true, nil
}

View File

@ -1,120 +0,0 @@
/*
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 cel
import (
"context"
"sync"
"time"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
// MatcherMetrics defines methods for reporting matchCondition metrics
type MatcherMetrics interface {
// RecordAuthorizationMatchConditionEvaluation records the total time taken to evaluate matchConditions for an Authorize() call to the given authorizer
RecordAuthorizationMatchConditionEvaluation(ctx context.Context, authorizerType, authorizerName string, elapsed time.Duration)
// RecordAuthorizationMatchConditionEvaluationFailure increments if any evaluation error was encountered evaluating matchConditions for an Authorize() call to the given authorizer
RecordAuthorizationMatchConditionEvaluationFailure(ctx context.Context, authorizerType, authorizerName string)
// RecordAuthorizationMatchConditionExclusion records increments when at least one matchCondition evaluates to false and excludes an Authorize() call to the given authorizer
RecordAuthorizationMatchConditionExclusion(ctx context.Context, authorizerType, authorizerName string)
}
type NoopMatcherMetrics struct{}
func (NoopMatcherMetrics) RecordAuthorizationMatchConditionEvaluation(ctx context.Context, authorizerType, authorizerName string, elapsed time.Duration) {
}
func (NoopMatcherMetrics) RecordAuthorizationMatchConditionEvaluationFailure(ctx context.Context, authorizerType, authorizerName string) {
}
func (NoopMatcherMetrics) RecordAuthorizationMatchConditionExclusion(ctx context.Context, authorizerType, authorizerName string) {
}
type matcherMetrics struct{}
func NewMatcherMetrics() MatcherMetrics {
RegisterMetrics()
return matcherMetrics{}
}
const (
namespace = "apiserver"
subsystem = "authorization"
)
var (
authorizationMatchConditionEvaluationErrorsTotal = metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "match_condition_evaluation_errors_total",
Help: "Total number of errors when an authorization webhook encounters a match condition error split by authorizer type and name.",
StabilityLevel: metrics.ALPHA,
},
[]string{"type", "name"},
)
authorizationMatchConditionExclusionsTotal = metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "match_condition_exclusions_total",
Help: "Total number of exclusions when an authorization webhook is skipped because match conditions exclude it.",
StabilityLevel: metrics.ALPHA,
},
[]string{"type", "name"},
)
authorizationMatchConditionEvaluationSeconds = metrics.NewHistogramVec(
&metrics.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "match_condition_evaluation_seconds",
Help: "Authorization match condition evaluation time in seconds, split by authorizer type and name.",
Buckets: []float64{0.001, 0.005, 0.01, 0.025, 0.1, 0.2, 0.25},
StabilityLevel: metrics.ALPHA,
},
[]string{"type", "name"},
)
)
var registerMetrics sync.Once
func RegisterMetrics() {
registerMetrics.Do(func() {
legacyregistry.MustRegister(authorizationMatchConditionEvaluationErrorsTotal)
legacyregistry.MustRegister(authorizationMatchConditionExclusionsTotal)
legacyregistry.MustRegister(authorizationMatchConditionEvaluationSeconds)
})
}
func ResetMetricsForTest() {
authorizationMatchConditionEvaluationErrorsTotal.Reset()
authorizationMatchConditionExclusionsTotal.Reset()
authorizationMatchConditionEvaluationSeconds.Reset()
}
func (matcherMetrics) RecordAuthorizationMatchConditionEvaluationFailure(ctx context.Context, authorizerType, authorizerName string) {
authorizationMatchConditionEvaluationErrorsTotal.WithContext(ctx).WithLabelValues(authorizerType, authorizerName).Inc()
}
func (matcherMetrics) RecordAuthorizationMatchConditionExclusion(ctx context.Context, authorizerType, authorizerName string) {
authorizationMatchConditionExclusionsTotal.WithContext(ctx).WithLabelValues(authorizerType, authorizerName).Inc()
}
func (matcherMetrics) RecordAuthorizationMatchConditionEvaluation(ctx context.Context, authorizerType, authorizerName string, elapsed time.Duration) {
elapsedSeconds := elapsed.Seconds()
authorizationMatchConditionEvaluationSeconds.WithContext(ctx).WithLabelValues(authorizerType, authorizerName).Observe(elapsedSeconds)
}

View File

@ -1,18 +0,0 @@
/*
Copyright 2018 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 path contains an authorizer that allows certain paths and path prefixes.
package path

View File

@ -1,68 +0,0 @@
/*
Copyright 2018 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 path
import (
"context"
"fmt"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// NewAuthorizer returns an authorizer which accepts a given set of paths.
// Each path is either a fully matching path or it ends in * in case a prefix match is done. A leading / is optional.
func NewAuthorizer(alwaysAllowPaths []string) (authorizer.Authorizer, error) {
var prefixes []string
paths := sets.NewString()
for _, p := range alwaysAllowPaths {
p = strings.TrimPrefix(p, "/")
if len(p) == 0 {
// matches "/"
paths.Insert(p)
continue
}
if strings.ContainsRune(p[:len(p)-1], '*') {
return nil, fmt.Errorf("only trailing * allowed in %q", p)
}
if strings.HasSuffix(p, "*") {
prefixes = append(prefixes, p[:len(p)-1])
} else {
paths.Insert(p)
}
}
return authorizer.AuthorizerFunc(func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
if a.IsResourceRequest() {
return authorizer.DecisionNoOpinion, "", nil
}
pth := strings.TrimPrefix(a.GetPath(), "/")
if paths.Has(pth) {
return authorizer.DecisionAllow, "", nil
}
for _, prefix := range prefixes {
if strings.HasPrefix(pth, prefix) {
return authorizer.DecisionAllow, "", nil
}
}
return authorizer.DecisionNoOpinion, "", nil
}), nil
}

View File

@ -1,106 +0,0 @@
/*
Copyright 2014 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 union implements an authorizer that combines multiple subauthorizer.
// The union authorizer iterates over each subauthorizer and returns the first
// decision that is either an Allow decision or a Deny decision. If a
// subauthorizer returns a NoOpinion, then the union authorizer moves onto the
// next authorizer or, if the subauthorizer was the last authorizer, returns
// NoOpinion as the aggregate decision. I.e. union authorizer creates an
// aggregate decision and supports short-circuit allows and denies from
// subauthorizers.
package union
import (
"context"
"strings"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// unionAuthzHandler authorizer against a chain of authorizer.Authorizer
type unionAuthzHandler []authorizer.Authorizer
// New returns an authorizer that authorizes against a chain of authorizer.Authorizer objects
func New(authorizationHandlers ...authorizer.Authorizer) authorizer.Authorizer {
return unionAuthzHandler(authorizationHandlers)
}
// Authorizes against a chain of authorizer.Authorizer objects and returns nil if successful and returns error if unsuccessful
func (authzHandler unionAuthzHandler) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
var (
errlist []error
reasonlist []string
)
for _, currAuthzHandler := range authzHandler {
decision, reason, err := currAuthzHandler.Authorize(ctx, a)
if err != nil {
errlist = append(errlist, err)
}
if len(reason) != 0 {
reasonlist = append(reasonlist, reason)
}
switch decision {
case authorizer.DecisionAllow, authorizer.DecisionDeny:
return decision, reason, err
case authorizer.DecisionNoOpinion:
// continue to the next authorizer
}
}
return authorizer.DecisionNoOpinion, strings.Join(reasonlist, "\n"), utilerrors.NewAggregate(errlist)
}
// unionAuthzRulesHandler authorizer against a chain of authorizer.RuleResolver
type unionAuthzRulesHandler []authorizer.RuleResolver
// NewRuleResolvers returns an authorizer that authorizes against a chain of authorizer.Authorizer objects
func NewRuleResolvers(authorizationHandlers ...authorizer.RuleResolver) authorizer.RuleResolver {
return unionAuthzRulesHandler(authorizationHandlers)
}
// RulesFor against a chain of authorizer.RuleResolver objects and returns nil if successful and returns error if unsuccessful
func (authzHandler unionAuthzRulesHandler) RulesFor(ctx context.Context, user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
var (
errList []error
resourceRulesList []authorizer.ResourceRuleInfo
nonResourceRulesList []authorizer.NonResourceRuleInfo
)
incompleteStatus := false
for _, currAuthzHandler := range authzHandler {
resourceRules, nonResourceRules, incomplete, err := currAuthzHandler.RulesFor(ctx, user, namespace)
if incomplete {
incompleteStatus = true
}
if err != nil {
errList = append(errList, err)
}
if len(resourceRules) > 0 {
resourceRulesList = append(resourceRulesList, resourceRules...)
}
if len(nonResourceRules) > 0 {
nonResourceRulesList = append(nonResourceRulesList, nonResourceRules...)
}
}
return resourceRulesList, nonResourceRulesList, incompleteStatus, utilerrors.NewAggregate(errList)
}

View File

@ -1,229 +0,0 @@
/*
Copyright 2023 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 openapi
import (
"github.com/google/cel-go/common/types/ref"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var _ common.Schema = (*Schema)(nil)
var _ common.SchemaOrBool = (*SchemaOrBool)(nil)
type Schema struct {
Schema *spec.Schema
}
type SchemaOrBool struct {
SchemaOrBool *spec.SchemaOrBool
}
func (sb *SchemaOrBool) Schema() common.Schema {
return &Schema{Schema: sb.SchemaOrBool.Schema}
}
func (sb *SchemaOrBool) Allows() bool {
return sb.SchemaOrBool.Allows
}
func (s *Schema) Type() string {
if len(s.Schema.Type) == 0 {
return ""
}
return s.Schema.Type[0]
}
func (s *Schema) Format() string {
return s.Schema.Format
}
func (s *Schema) Pattern() string {
return s.Schema.Pattern
}
func (s *Schema) Items() common.Schema {
if s.Schema.Items == nil || s.Schema.Items.Schema == nil {
return nil
}
return &Schema{Schema: s.Schema.Items.Schema}
}
func (s *Schema) Properties() map[string]common.Schema {
if s.Schema.Properties == nil {
return nil
}
res := make(map[string]common.Schema, len(s.Schema.Properties))
for n, prop := range s.Schema.Properties {
// map value is unaddressable, create a shallow copy
// this is a shallow non-recursive copy
s := prop
res[n] = &Schema{Schema: &s}
}
return res
}
func (s *Schema) AdditionalProperties() common.SchemaOrBool {
if s.Schema.AdditionalProperties == nil {
return nil
}
return &SchemaOrBool{SchemaOrBool: s.Schema.AdditionalProperties}
}
func (s *Schema) Default() any {
return s.Schema.Default
}
func (s *Schema) Minimum() *float64 {
return s.Schema.Minimum
}
func (s *Schema) IsExclusiveMinimum() bool {
return s.Schema.ExclusiveMinimum
}
func (s *Schema) Maximum() *float64 {
return s.Schema.Maximum
}
func (s *Schema) IsExclusiveMaximum() bool {
return s.Schema.ExclusiveMaximum
}
func (s *Schema) MultipleOf() *float64 {
return s.Schema.MultipleOf
}
func (s *Schema) UniqueItems() bool {
return s.Schema.UniqueItems
}
func (s *Schema) MinItems() *int64 {
return s.Schema.MinItems
}
func (s *Schema) MaxItems() *int64 {
return s.Schema.MaxItems
}
func (s *Schema) MinLength() *int64 {
return s.Schema.MinLength
}
func (s *Schema) MaxLength() *int64 {
return s.Schema.MaxLength
}
func (s *Schema) MinProperties() *int64 {
return s.Schema.MinProperties
}
func (s *Schema) MaxProperties() *int64 {
return s.Schema.MaxProperties
}
func (s *Schema) Required() []string {
return s.Schema.Required
}
func (s *Schema) Enum() []any {
return s.Schema.Enum
}
func (s *Schema) Nullable() bool {
return s.Schema.Nullable
}
func (s *Schema) AllOf() []common.Schema {
var res []common.Schema
for _, nestedSchema := range s.Schema.AllOf {
nestedSchema := nestedSchema
res = append(res, &Schema{&nestedSchema})
}
return res
}
func (s *Schema) AnyOf() []common.Schema {
var res []common.Schema
for _, nestedSchema := range s.Schema.AnyOf {
nestedSchema := nestedSchema
res = append(res, &Schema{&nestedSchema})
}
return res
}
func (s *Schema) OneOf() []common.Schema {
var res []common.Schema
for _, nestedSchema := range s.Schema.OneOf {
nestedSchema := nestedSchema
res = append(res, &Schema{&nestedSchema})
}
return res
}
func (s *Schema) Not() common.Schema {
if s.Schema.Not == nil {
return nil
}
return &Schema{s.Schema.Not}
}
func (s *Schema) IsXIntOrString() bool {
return isXIntOrString(s.Schema)
}
func (s *Schema) IsXEmbeddedResource() bool {
return isXEmbeddedResource(s.Schema)
}
func (s *Schema) IsXPreserveUnknownFields() bool {
return isXPreserveUnknownFields(s.Schema)
}
func (s *Schema) XListType() string {
return getXListType(s.Schema)
}
func (s *Schema) XMapType() string {
return getXMapType(s.Schema)
}
func (s *Schema) XListMapKeys() []string {
return getXListMapKeys(s.Schema)
}
func (s *Schema) XValidations() []common.ValidationRule {
return getXValidations(s.Schema)
}
func (s *Schema) WithTypeAndObjectMeta() common.Schema {
return &Schema{common.WithTypeAndObjectMeta(s.Schema)}
}
func UnstructuredToVal(unstructured any, schema *spec.Schema) ref.Val {
return common.UnstructuredToVal(unstructured, &Schema{schema})
}
func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType {
return common.SchemaDeclType(&Schema{Schema: s}, isResourceRoot)
}
func MakeMapList(sts *spec.Schema, items []interface{}) (rv common.MapList) {
return common.MakeMapList(&Schema{Schema: sts}, items)
}

View File

@ -1,107 +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 openapi
import (
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var intOrStringFormat = intstr.IntOrString{}.OpenAPISchemaFormat()
func isExtension(schema *spec.Schema, key string) bool {
v, ok := schema.Extensions.GetBool(key)
return v && ok
}
func isXIntOrString(schema *spec.Schema) bool {
// built-in types have the Format while CRDs use extension
// both are valid, checking both
return schema.Format == intOrStringFormat || isExtension(schema, extIntOrString)
}
func isXEmbeddedResource(schema *spec.Schema) bool {
return isExtension(schema, extEmbeddedResource)
}
func isXPreserveUnknownFields(schema *spec.Schema) bool {
return isExtension(schema, extPreserveUnknownFields)
}
func getXListType(schema *spec.Schema) string {
s, _ := schema.Extensions.GetString(extListType)
return s
}
func getXMapType(schema *spec.Schema) string {
s, _ := schema.Extensions.GetString(extMapType)
return s
}
func getXListMapKeys(schema *spec.Schema) []string {
mapKeys, ok := schema.Extensions.GetStringSlice(extListMapKeys)
if !ok {
return nil
}
return mapKeys
}
type ValidationRule struct {
RuleField string `json:"rule"`
MessageField string `json:"message"`
MessageExpressionField string `json:"messageExpression"`
PathField string `json:"fieldPath"`
}
func (v ValidationRule) Rule() string {
return v.RuleField
}
func (v ValidationRule) Message() string {
return v.MessageField
}
func (v ValidationRule) FieldPath() string {
return v.PathField
}
func (v ValidationRule) MessageExpression() string {
return v.MessageExpressionField
}
// TODO: simplify
func getXValidations(schema *spec.Schema) []common.ValidationRule {
var rules []ValidationRule
err := schema.Extensions.GetObject(extValidations, &rules)
if err != nil {
return nil
}
results := make([]common.ValidationRule, len(rules))
for i, rule := range rules {
results[i] = rule
}
return results
}
const extIntOrString = "x-kubernetes-int-or-string"
const extEmbeddedResource = "x-kubernetes-embedded-resource"
const extPreserveUnknownFields = "x-kubernetes-preserve-unknown-fields"
const extListType = "x-kubernetes-list-type"
const extMapType = "x-kubernetes-map-type"
const extListMapKeys = "x-kubernetes-list-map-keys"
const extValidations = "x-kubernetes-validations"

View File

@ -1,2 +0,0 @@
approvers:
- apelisse

View File

@ -1,134 +0,0 @@
/*
Copyright 2020 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 deprecation
import (
"fmt"
"regexp"
"strconv"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
)
type apiLifecycleDeprecated interface {
APILifecycleDeprecated() (major, minor int)
}
type apiLifecycleRemoved interface {
APILifecycleRemoved() (major, minor int)
}
type apiLifecycleReplacement interface {
APILifecycleReplacement() schema.GroupVersionKind
}
// extract all digits at the beginning of the string
var leadingDigits = regexp.MustCompile(`^(\d+)`)
// MajorMinor parses a numeric major/minor version from the provided version info.
// The minor version drops all characters after the first non-digit character:
//
// version.Info{Major:"1", Minor:"2+"} -> 1,2
// version.Info{Major:"1", Minor:"2.3-build4"} -> 1,2
func MajorMinor(v version.Info) (int, int, error) {
major, err := strconv.Atoi(v.Major)
if err != nil {
return 0, 0, err
}
minor, err := strconv.Atoi(leadingDigits.FindString(v.Minor))
if err != nil {
return 0, 0, err
}
return major, minor, nil
}
// IsDeprecated returns true if obj implements APILifecycleDeprecated() and returns
// a major/minor version that is non-zero and is <= the specified current major/minor version.
func IsDeprecated(obj runtime.Object, currentMajor, currentMinor int) bool {
deprecated, isDeprecated := obj.(apiLifecycleDeprecated)
if !isDeprecated {
return false
}
deprecatedMajor, deprecatedMinor := deprecated.APILifecycleDeprecated()
// no deprecation version expressed
if deprecatedMajor == 0 && deprecatedMinor == 0 {
return false
}
// no current version info available
if currentMajor == 0 && currentMinor == 0 {
return true
}
// compare deprecation version to current version
if deprecatedMajor > currentMajor {
return false
}
if deprecatedMajor == currentMajor && deprecatedMinor > currentMinor {
return false
}
return true
}
// RemovedRelease returns the major/minor version in which the given object is unavailable (in the form "<major>.<minor>")
// if the object implements APILifecycleRemoved() to indicate a non-zero removal version, and returns an empty string otherwise.
func RemovedRelease(obj runtime.Object) string {
if removed, hasRemovalInfo := obj.(apiLifecycleRemoved); hasRemovalInfo {
removedMajor, removedMinor := removed.APILifecycleRemoved()
if removedMajor != 0 || removedMinor != 0 {
return fmt.Sprintf("%d.%d", removedMajor, removedMinor)
}
}
return ""
}
// WarningMessage returns a human-readable deprecation warning if the object implements APILifecycleDeprecated()
// to indicate a non-zero deprecated major/minor version and has a populated GetObjectKind().GroupVersionKind().
func WarningMessage(obj runtime.Object) string {
deprecated, isDeprecated := obj.(apiLifecycleDeprecated)
if !isDeprecated {
return ""
}
deprecatedMajor, deprecatedMinor := deprecated.APILifecycleDeprecated()
if deprecatedMajor == 0 && deprecatedMinor == 0 {
return ""
}
gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Empty() {
return ""
}
deprecationWarning := fmt.Sprintf("%s %s is deprecated in v%d.%d+", gvk.GroupVersion().String(), gvk.Kind, deprecatedMajor, deprecatedMinor)
if removed, hasRemovalInfo := obj.(apiLifecycleRemoved); hasRemovalInfo {
removedMajor, removedMinor := removed.APILifecycleRemoved()
if removedMajor != 0 || removedMinor != 0 {
deprecationWarning = deprecationWarning + fmt.Sprintf(", unavailable in v%d.%d+", removedMajor, removedMinor)
}
}
if replaced, hasReplacement := obj.(apiLifecycleReplacement); hasReplacement {
replacement := replaced.APILifecycleReplacement()
if !replacement.Empty() {
deprecationWarning = deprecationWarning + fmt.Sprintf("; use %s %s", replacement.GroupVersion().String(), replacement.Kind)
}
}
return deprecationWarning
}

View File

@ -1,5 +0,0 @@
# See the OWNERS docs at https://go.k8s.io/owners
reviewers:
- alexzielenski
- jefftree

View File

@ -1,72 +0,0 @@
/*
Copyright 2016 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 discovery
import (
"net"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Addresses interface {
ServerAddressByClientCIDRs(net.IP) []metav1.ServerAddressByClientCIDR
}
// DefaultAddresses is a default implementation of Addresses that will work in most cases
type DefaultAddresses struct {
// CIDRRules is a list of CIDRs and Addresses to use if a client is in the range
CIDRRules []CIDRRule
// DefaultAddress is the address (hostname or IP and port) that should be used in
// if no CIDR matches more specifically.
DefaultAddress string
}
// CIDRRule is a rule for adding an alternate path to the master based on matching CIDR
type CIDRRule struct {
IPRange net.IPNet
// Address is the address (hostname or IP and port) that should be used in
// if this CIDR matches
Address string
}
func (d DefaultAddresses) ServerAddressByClientCIDRs(clientIP net.IP) []metav1.ServerAddressByClientCIDR {
addressCIDRMap := []metav1.ServerAddressByClientCIDR{
{
ClientCIDR: "0.0.0.0/0",
ServerAddress: d.DefaultAddress,
},
}
for _, rule := range d.CIDRRules {
addressCIDRMap = append(addressCIDRMap, rule.ServerAddressByClientCIDRs(clientIP)...)
}
return addressCIDRMap
}
func (d CIDRRule) ServerAddressByClientCIDRs(clientIP net.IP) []metav1.ServerAddressByClientCIDR {
addressCIDRMap := []metav1.ServerAddressByClientCIDR{}
if d.IPRange.Contains(clientIP) {
addressCIDRMap = append(addressCIDRMap, metav1.ServerAddressByClientCIDR{
ClientCIDR: d.IPRange.String(),
ServerAddress: d.Address,
})
}
return addressCIDRMap
}

View File

@ -1,85 +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 aggregated
import (
"crypto/sha512"
"encoding/json"
"fmt"
"net/http"
"strconv"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
)
// This file exposes helper functions used for calculating the E-Tag header
// used in discovery endpoint responses
// Attaches Cache-Busting functionality to an endpoint
// - Sets ETag header to provided hash
// - Replies with 304 Not Modified, if If-None-Match header matches hash
//
// hash should be the value of calculateETag on object. If hash is empty, then
// the object is simply serialized without E-Tag functionality
func ServeHTTPWithETag(
object runtime.Object,
hash string,
targetGV schema.GroupVersion,
serializer runtime.NegotiatedSerializer,
w http.ResponseWriter,
req *http.Request,
) {
// ETag must be enclosed in double quotes:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
quotedHash := strconv.Quote(hash)
w.Header().Set("ETag", quotedHash)
w.Header().Set("Vary", "Accept")
w.Header().Set("Cache-Control", "public")
// If Request includes If-None-Match and matches hash, reply with 304
// Otherwise, we delegate to the handler for actual content
//
// According to documentation, An Etag within an If-None-Match
// header will be enclosed within double quotes:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
if clientCachedHash := req.Header.Get("If-None-Match"); quotedHash == clientCachedHash {
w.WriteHeader(http.StatusNotModified)
return
}
responsewriters.WriteObjectNegotiated(
serializer,
DiscoveryEndpointRestrictions,
targetGV,
w,
req,
http.StatusOK,
object,
true,
)
}
func calculateETag(resources interface{}) (string, error) {
serialized, err := json.Marshal(resources)
if err != nil {
return "", err
}
return fmt.Sprintf("%X", sha512.Sum512(serialized)), nil
}

View File

@ -1,175 +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 aggregated
import (
"context"
"errors"
"net/http"
"reflect"
"sync"
"time"
"github.com/emicklei/go-restful/v3"
"github.com/google/go-cmp/cmp" //nolint:depguard
apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
)
type FakeResourceManager interface {
ResourceManager
Expect() ResourceManager
HasExpectedNumberActions() bool
Validate() error
WaitForActions(ctx context.Context, timeout time.Duration) error
}
func NewFakeResourceManager() FakeResourceManager {
return &fakeResourceManager{}
}
// a resource manager with helper functions for checking the actions
// match expected. For Use in tests
type fakeResourceManager struct {
recorderResourceManager
expect recorderResourceManager
}
// a resource manager which instead of managing a discovery document,
// simply records the calls to its interface functoins for testing
type recorderResourceManager struct {
lock sync.RWMutex
Actions []recorderResourceManagerAction
}
var _ ResourceManager = &fakeResourceManager{}
var _ ResourceManager = &recorderResourceManager{}
// Storage type for a call to the resource manager
type recorderResourceManagerAction struct {
Type string
Group string
Version string
Value interface{}
}
func (f *fakeResourceManager) Expect() ResourceManager {
return &f.expect
}
func (f *fakeResourceManager) HasExpectedNumberActions() bool {
f.lock.RLock()
defer f.lock.RUnlock()
f.expect.lock.RLock()
defer f.expect.lock.RUnlock()
return len(f.Actions) >= len(f.expect.Actions)
}
func (f *fakeResourceManager) Validate() error {
f.lock.RLock()
defer f.lock.RUnlock()
f.expect.lock.RLock()
defer f.expect.lock.RUnlock()
if !reflect.DeepEqual(f.expect.Actions, f.Actions) {
return errors.New(cmp.Diff(f.expect.Actions, f.Actions))
}
return nil
}
func (f *fakeResourceManager) WaitForActions(ctx context.Context, timeout time.Duration) error {
err := wait.PollImmediateWithContext(
ctx,
100*time.Millisecond, // try every 100ms
timeout, // timeout after timeout
func(ctx context.Context) (done bool, err error) {
if f.HasExpectedNumberActions() {
return true, f.Validate()
}
return false, nil
})
return err
}
func (f *recorderResourceManager) SetGroupVersionPriority(gv metav1.GroupVersion, grouppriority, versionpriority int) {
f.lock.Lock()
defer f.lock.Unlock()
f.Actions = append(f.Actions, recorderResourceManagerAction{
Type: "SetGroupVersionPriority",
Group: gv.Group,
Version: gv.Version,
Value: versionpriority,
})
}
func (f *recorderResourceManager) AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery) {
f.lock.Lock()
defer f.lock.Unlock()
f.Actions = append(f.Actions, recorderResourceManagerAction{
Type: "AddGroupVersion",
Group: groupName,
Value: value,
})
}
func (f *recorderResourceManager) RemoveGroup(groupName string) {
f.lock.Lock()
defer f.lock.Unlock()
f.Actions = append(f.Actions, recorderResourceManagerAction{
Type: "RemoveGroup",
Group: groupName,
})
}
func (f *recorderResourceManager) RemoveGroupVersion(gv metav1.GroupVersion) {
f.lock.Lock()
defer f.lock.Unlock()
f.Actions = append(f.Actions, recorderResourceManagerAction{
Type: "RemoveGroupVersion",
Group: gv.Group,
Version: gv.Version,
})
}
func (f *recorderResourceManager) SetGroups(values []apidiscoveryv2.APIGroupDiscovery) {
f.lock.Lock()
defer f.lock.Unlock()
f.Actions = append(f.Actions, recorderResourceManagerAction{
Type: "SetGroups",
Value: values,
})
}
func (f *recorderResourceManager) WebService() *restful.WebService {
panic("unimplemented")
}
func (f *recorderResourceManager) ServeHTTP(http.ResponseWriter, *http.Request) {
panic("unimplemented")
}
func (f *recorderResourceManager) WithSource(source Source) ResourceManager {
panic("unimplemented")
}

View File

@ -1,577 +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 aggregated
import (
"fmt"
"net/http"
"reflect"
"sort"
"sync"
apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/version"
apidiscoveryv2conversion "k8s.io/apiserver/pkg/apis/apidiscovery/v2"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/metrics"
"sync/atomic"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"
)
type Source uint
// The GroupVersion from the lowest Source takes precedence
const (
AggregatorSource Source = 0
BuiltinSource Source = 100
CRDSource Source = 200
)
// This handler serves the /apis endpoint for an aggregated list of
// api resources indexed by their group version.
type ResourceManager interface {
// Adds knowledge of the given groupversion to the discovery document
// If it was already being tracked, updates the stored APIVersionDiscovery
// Thread-safe
AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery)
// Sets a priority to be used while sorting a specific group and
// group-version. If two versions report different priorities for
// the group, the higher one will be used. If the group is not
// known, the priority is ignored. The priority for this version
// is forgotten once the group-version is forgotten
SetGroupVersionPriority(gv metav1.GroupVersion, grouppriority, versionpriority int)
// Removes all group versions for a given group
// Thread-safe
RemoveGroup(groupName string)
// Removes a specific groupversion. If all versions of a group have been
// removed, then the entire group is unlisted.
// Thread-safe
RemoveGroupVersion(gv metav1.GroupVersion)
// Resets the manager's known list of group-versions and replaces them
// with the given groups
// Thread-Safe
SetGroups([]apidiscoveryv2.APIGroupDiscovery)
// Returns the same resource manager using a different source
// The source is used to decide how to de-duplicate groups.
// The group from the least-numbered source is used
WithSource(source Source) ResourceManager
http.Handler
}
type resourceManager struct {
source Source
*resourceDiscoveryManager
}
func (rm resourceManager) AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery) {
rm.resourceDiscoveryManager.AddGroupVersion(rm.source, groupName, value)
}
func (rm resourceManager) SetGroupVersionPriority(gv metav1.GroupVersion, grouppriority, versionpriority int) {
rm.resourceDiscoveryManager.SetGroupVersionPriority(rm.source, gv, grouppriority, versionpriority)
}
func (rm resourceManager) RemoveGroup(groupName string) {
rm.resourceDiscoveryManager.RemoveGroup(rm.source, groupName)
}
func (rm resourceManager) RemoveGroupVersion(gv metav1.GroupVersion) {
rm.resourceDiscoveryManager.RemoveGroupVersion(rm.source, gv)
}
func (rm resourceManager) SetGroups(groups []apidiscoveryv2.APIGroupDiscovery) {
rm.resourceDiscoveryManager.SetGroups(rm.source, groups)
}
func (rm resourceManager) WithSource(source Source) ResourceManager {
return resourceManager{
source: source,
resourceDiscoveryManager: rm.resourceDiscoveryManager,
}
}
type groupKey struct {
name string
// Source identifies where this group came from and dictates which group
// among duplicates is chosen to be used for discovery.
source Source
}
type groupVersionKey struct {
metav1.GroupVersion
source Source
}
type resourceDiscoveryManager struct {
serializer runtime.NegotiatedSerializer
// cache is an atomic pointer to avoid the use of locks
cache atomic.Pointer[cachedGroupList]
serveHTTPFunc http.HandlerFunc
// Writes protected by the lock.
// List of all apigroups & resources indexed by the resource manager
lock sync.RWMutex
apiGroups map[groupKey]*apidiscoveryv2.APIGroupDiscovery
versionPriorities map[groupVersionKey]priorityInfo
}
type priorityInfo struct {
GroupPriorityMinimum int
VersionPriority int
}
func NewResourceManager(path string) ResourceManager {
scheme := runtime.NewScheme()
utilruntime.Must(apidiscoveryv2.AddToScheme(scheme))
utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme))
// Register conversion for apidiscovery
utilruntime.Must(apidiscoveryv2conversion.RegisterConversions(scheme))
codecs := serializer.NewCodecFactory(scheme)
rdm := &resourceDiscoveryManager{
serializer: codecs,
versionPriorities: make(map[groupVersionKey]priorityInfo),
}
rdm.serveHTTPFunc = metrics.InstrumentHandlerFunc("GET",
/* group = */ "",
/* version = */ "",
/* resource = */ "",
/* subresource = */ path,
/* scope = */ "",
/* component = */ metrics.APIServerComponent,
/* deprecated */ false,
/* removedRelease */ "",
rdm.serveHTTP)
return resourceManager{
source: BuiltinSource,
resourceDiscoveryManager: rdm,
}
}
func (rdm *resourceDiscoveryManager) SetGroupVersionPriority(source Source, gv metav1.GroupVersion, groupPriorityMinimum, versionPriority int) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
key := groupVersionKey{
GroupVersion: gv,
source: source,
}
rdm.versionPriorities[key] = priorityInfo{
GroupPriorityMinimum: groupPriorityMinimum,
VersionPriority: versionPriority,
}
rdm.cache.Store(nil)
}
func (rdm *resourceDiscoveryManager) SetGroups(source Source, groups []apidiscoveryv2.APIGroupDiscovery) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
rdm.apiGroups = nil
rdm.cache.Store(nil)
for _, group := range groups {
for _, version := range group.Versions {
rdm.addGroupVersionLocked(source, group.Name, version)
}
}
// Filter unused out priority entries
for gv := range rdm.versionPriorities {
key := groupKey{
source: source,
name: gv.Group,
}
entry, exists := rdm.apiGroups[key]
if !exists {
delete(rdm.versionPriorities, gv)
continue
}
containsVersion := false
for _, v := range entry.Versions {
if v.Version == gv.Version {
containsVersion = true
break
}
}
if !containsVersion {
delete(rdm.versionPriorities, gv)
}
}
}
func (rdm *resourceDiscoveryManager) AddGroupVersion(source Source, groupName string, value apidiscoveryv2.APIVersionDiscovery) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
rdm.addGroupVersionLocked(source, groupName, value)
}
func (rdm *resourceDiscoveryManager) addGroupVersionLocked(source Source, groupName string, value apidiscoveryv2.APIVersionDiscovery) {
if rdm.apiGroups == nil {
rdm.apiGroups = make(map[groupKey]*apidiscoveryv2.APIGroupDiscovery)
}
key := groupKey{
source: source,
name: groupName,
}
if existing, groupExists := rdm.apiGroups[key]; groupExists {
// If this version already exists, replace it
versionExists := false
// Not very efficient, but in practice there are generally not many versions
for i := range existing.Versions {
if existing.Versions[i].Version == value.Version {
// The new gv is the exact same as what is already in
// the map. This is a noop and cache should not be
// invalidated.
if reflect.DeepEqual(existing.Versions[i], value) {
return
}
existing.Versions[i] = value
versionExists = true
break
}
}
if !versionExists {
existing.Versions = append(existing.Versions, value)
}
} else {
group := &apidiscoveryv2.APIGroupDiscovery{
ObjectMeta: metav1.ObjectMeta{
Name: groupName,
},
Versions: []apidiscoveryv2.APIVersionDiscovery{value},
}
rdm.apiGroups[key] = group
}
klog.Infof("Adding GroupVersion %s %s to ResourceManager", groupName, value.Version)
gv := metav1.GroupVersion{Group: groupName, Version: value.Version}
gvKey := groupVersionKey{
GroupVersion: gv,
source: source,
}
if _, ok := rdm.versionPriorities[gvKey]; !ok {
rdm.versionPriorities[gvKey] = priorityInfo{
GroupPriorityMinimum: 1000,
VersionPriority: 15,
}
}
// Reset response document so it is recreated lazily
rdm.cache.Store(nil)
}
func (rdm *resourceDiscoveryManager) RemoveGroupVersion(source Source, apiGroup metav1.GroupVersion) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
key := groupKey{
source: source,
name: apiGroup.Group,
}
group, exists := rdm.apiGroups[key]
if !exists {
return
}
modified := false
for i := range group.Versions {
if group.Versions[i].Version == apiGroup.Version {
group.Versions = append(group.Versions[:i], group.Versions[i+1:]...)
modified = true
break
}
}
// If no modification was done, cache does not need to be cleared
if !modified {
return
}
gvKey := groupVersionKey{
GroupVersion: apiGroup,
source: source,
}
delete(rdm.versionPriorities, gvKey)
if len(group.Versions) == 0 {
delete(rdm.apiGroups, key)
}
// Reset response document so it is recreated lazily
rdm.cache.Store(nil)
}
func (rdm *resourceDiscoveryManager) RemoveGroup(source Source, groupName string) {
rdm.lock.Lock()
defer rdm.lock.Unlock()
key := groupKey{
source: source,
name: groupName,
}
delete(rdm.apiGroups, key)
for k := range rdm.versionPriorities {
if k.Group == groupName && k.source == source {
delete(rdm.versionPriorities, k)
}
}
// Reset response document so it is recreated lazily
rdm.cache.Store(nil)
}
// Prepares the api group list for serving by converting them from map into
// list and sorting them according to insertion order
func (rdm *resourceDiscoveryManager) calculateAPIGroupsLocked() []apidiscoveryv2.APIGroupDiscovery {
regenerationCounter.Inc()
// Re-order the apiGroups by their priority.
groups := []apidiscoveryv2.APIGroupDiscovery{}
groupsToUse := map[string]apidiscoveryv2.APIGroupDiscovery{}
sourcesUsed := map[metav1.GroupVersion]Source{}
for key, group := range rdm.apiGroups {
if existing, ok := groupsToUse[key.name]; ok {
for _, v := range group.Versions {
gv := metav1.GroupVersion{Group: key.name, Version: v.Version}
// Skip groupversions we've already seen before. Only DefaultSource
// takes precedence
if usedSource, seen := sourcesUsed[gv]; seen && key.source >= usedSource {
continue
} else if seen {
// Find the index of the duplicate version and replace
for i := 0; i < len(existing.Versions); i++ {
if existing.Versions[i].Version == v.Version {
existing.Versions[i] = v
break
}
}
} else {
// New group-version, just append
existing.Versions = append(existing.Versions, v)
}
sourcesUsed[gv] = key.source
groupsToUse[key.name] = existing
}
// Check to see if we have overlapping versions. If we do, take the one
// with highest source precedence
} else {
groupsToUse[key.name] = *group.DeepCopy()
for _, v := range group.Versions {
gv := metav1.GroupVersion{Group: key.name, Version: v.Version}
sourcesUsed[gv] = key.source
}
}
}
for _, group := range groupsToUse {
// Re-order versions based on their priority. Use kube-aware string
// comparison as a tie breaker
sort.SliceStable(group.Versions, func(i, j int) bool {
iVersion := group.Versions[i].Version
jVersion := group.Versions[j].Version
iGV := metav1.GroupVersion{Group: group.Name, Version: iVersion}
jGV := metav1.GroupVersion{Group: group.Name, Version: jVersion}
iSource := sourcesUsed[iGV]
jSource := sourcesUsed[jGV]
iPriority := rdm.versionPriorities[groupVersionKey{iGV, iSource}].VersionPriority
jPriority := rdm.versionPriorities[groupVersionKey{jGV, jSource}].VersionPriority
// Sort by version string comparator if priority is equal
if iPriority == jPriority {
return version.CompareKubeAwareVersionStrings(iVersion, jVersion) > 0
}
// i sorts before j if it has a higher priority
return iPriority > jPriority
})
groups = append(groups, group)
}
// For each group, determine the highest minimum group priority and use that
priorities := map[string]int{}
for gv, info := range rdm.versionPriorities {
if source := sourcesUsed[gv.GroupVersion]; source != gv.source {
continue
}
if existing, exists := priorities[gv.Group]; exists {
if existing < info.GroupPriorityMinimum {
priorities[gv.Group] = info.GroupPriorityMinimum
}
} else {
priorities[gv.Group] = info.GroupPriorityMinimum
}
}
sort.SliceStable(groups, func(i, j int) bool {
iName := groups[i].Name
jName := groups[j].Name
// Default to 0 priority by default
iPriority := priorities[iName]
jPriority := priorities[jName]
// Sort discovery based on apiservice priority.
// Duplicated from staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helpers.go
if iPriority == jPriority {
// Equal priority uses name to break ties
return iName < jName
}
// i sorts before j if it has a higher priority
return iPriority > jPriority
})
return groups
}
// Fetches from cache if it exists. If cache is empty, create it.
func (rdm *resourceDiscoveryManager) fetchFromCache() *cachedGroupList {
rdm.lock.RLock()
defer rdm.lock.RUnlock()
cacheLoad := rdm.cache.Load()
if cacheLoad != nil {
return cacheLoad
}
response := apidiscoveryv2.APIGroupDiscoveryList{
Items: rdm.calculateAPIGroupsLocked(),
}
etag, err := calculateETag(response)
if err != nil {
klog.Errorf("failed to calculate etag for discovery document: %s", etag)
etag = ""
}
cached := &cachedGroupList{
cachedResponse: response,
cachedResponseETag: etag,
}
rdm.cache.Store(cached)
return cached
}
type cachedGroupList struct {
cachedResponse apidiscoveryv2.APIGroupDiscoveryList
// etag is calculated based on a SHA hash of only the JSON object.
// A response via different Accept encodings (eg: protobuf, json) will
// yield the same etag. This is okay because Accept is part of the Vary header.
// Per RFC7231 a client must only cache a response etag pair if the header field
// matches as indicated by the Vary field. Thus, protobuf and json and other Accept
// encodings will not be cached as the same response despite having the same etag.
cachedResponseETag string
}
func (rdm *resourceDiscoveryManager) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
rdm.serveHTTPFunc(resp, req)
}
func (rdm *resourceDiscoveryManager) serveHTTP(resp http.ResponseWriter, req *http.Request) {
cache := rdm.fetchFromCache()
response := cache.cachedResponse
etag := cache.cachedResponseETag
mediaType, _, err := negotiation.NegotiateOutputMediaType(req, rdm.serializer, DiscoveryEndpointRestrictions)
if err != nil {
// Should never happen. wrapper.go will only proxy requests to this
// handler if the media type passes DiscoveryEndpointRestrictions
utilruntime.HandleError(err)
resp.WriteHeader(http.StatusInternalServerError)
return
}
var targetGV schema.GroupVersion
if mediaType.Convert == nil ||
(mediaType.Convert.GroupVersion() != apidiscoveryv2.SchemeGroupVersion &&
mediaType.Convert.GroupVersion() != apidiscoveryv2beta1.SchemeGroupVersion) {
utilruntime.HandleError(fmt.Errorf("expected aggregated discovery group version, got group: %s, version %s", mediaType.Convert.Group, mediaType.Convert.Version))
resp.WriteHeader(http.StatusInternalServerError)
return
}
if mediaType.Convert.GroupVersion() == apidiscoveryv2beta1.SchemeGroupVersion &&
utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryRemoveBetaType) {
klog.Errorf("aggregated discovery version v2beta1 is removed. Please update to use v2")
resp.WriteHeader(http.StatusNotFound)
return
}
targetGV = mediaType.Convert.GroupVersion()
if len(etag) > 0 {
// Use proper e-tag headers if one is available
ServeHTTPWithETag(
&response,
etag,
targetGV,
rdm.serializer,
resp,
req,
)
} else {
// Default to normal response in rare case etag is
// not cached with the object for some reason.
responsewriters.WriteObjectNegotiated(
rdm.serializer,
DiscoveryEndpointRestrictions,
targetGV,
resp,
req,
http.StatusOK,
&response,
true,
)
}
}

View File

@ -1,36 +0,0 @@
/*
Copyright 2023 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 aggregated
import (
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
var (
regenerationCounter = metrics.NewCounter(
&metrics.CounterOpts{
Name: "aggregator_discovery_aggregation_count_total",
Help: "Counter of number of times discovery was aggregated",
StabilityLevel: metrics.ALPHA,
},
)
)
func init() {
legacyregistry.MustRegister(regenerationCounter)
}

View File

@ -1,49 +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 aggregated
import (
"k8s.io/apimachinery/pkg/runtime/schema"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
// Interface is from "k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
// DiscoveryEndpointRestrictions allows requests to /apis to provide a Content Negotiation GVK for aggregated discovery.
var DiscoveryEndpointRestrictions = discoveryEndpointRestrictions{}
type discoveryEndpointRestrictions struct{}
func (discoveryEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool {
return IsAggregatedDiscoveryGVK(gvk)
}
func (discoveryEndpointRestrictions) AllowsServerVersion(string) bool { return false }
func (discoveryEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" }
// IsAggregatedDiscoveryGVK checks if a provided GVK is the GVK for serving aggregated discovery.
func IsAggregatedDiscoveryGVK(gvk *schema.GroupVersionKind) bool {
if gvk != nil {
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryRemoveBetaType) {
return gvk.Group == "apidiscovery.k8s.io" && gvk.Version == "v2" && gvk.Kind == "APIGroupDiscoveryList"
}
return gvk.Group == "apidiscovery.k8s.io" && (gvk.Version == "v2beta1" || gvk.Version == "v2") && gvk.Kind == "APIGroupDiscoveryList"
}
return false
}

View File

@ -1,77 +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 aggregated
import (
"net/http"
apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"github.com/emicklei/go-restful/v3"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
)
type WrappedHandler struct {
s runtime.NegotiatedSerializer
handler http.Handler
aggHandler http.Handler
}
func (wrapped *WrappedHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
mediaType, _ := negotiation.NegotiateMediaTypeOptions(req.Header.Get("Accept"), wrapped.s.SupportedMediaTypes(), DiscoveryEndpointRestrictions)
// mediaType.Convert looks at the request accept headers and is used to control whether the discovery document will be aggregated.
if IsAggregatedDiscoveryGVK(mediaType.Convert) {
wrapped.aggHandler.ServeHTTP(resp, req)
return
}
wrapped.handler.ServeHTTP(resp, req)
}
func (wrapped *WrappedHandler) restfulHandle(req *restful.Request, resp *restful.Response) {
wrapped.ServeHTTP(resp.ResponseWriter, req.Request)
}
func (wrapped *WrappedHandler) GenerateWebService(prefix string, returnType interface{}) *restful.WebService {
mediaTypes, _ := negotiation.MediaTypesForSerializer(wrapped.s)
ws := new(restful.WebService)
ws.Path(prefix)
ws.Doc("get available API versions")
ws.Route(ws.GET("/").To(wrapped.restfulHandle).
Doc("get available API versions").
Operation("getAPIVersions").
Produces(mediaTypes...).
Consumes(mediaTypes...).
Writes(returnType))
return ws
}
// WrapAggregatedDiscoveryToHandler wraps a handler with an option to
// emit the aggregated discovery by passing in the aggregated
// discovery type in content negotiation headers: eg: (Accept:
// application/json;v=v2;g=apidiscovery.k8s.io;as=APIGroupDiscoveryList)
func WrapAggregatedDiscoveryToHandler(handler http.Handler, aggHandler http.Handler) *WrappedHandler {
scheme := runtime.NewScheme()
utilruntime.Must(apidiscoveryv2.AddToScheme(scheme))
utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme))
codecs := serializer.NewCodecFactory(scheme)
return &WrappedHandler{codecs, handler, aggHandler}
}

Some files were not shown because too many files have changed in this diff Show More