VYPR
Critical severity9.6NVD Advisory· Published Mar 20, 2025· Updated Apr 15, 2026

CVE-2025-29922

CVE-2025-29922

Description

kcp is a Kubernetes-like control plane for form-factors and use-cases beyond Kubernetes and container workloads. Prior to 0.26.3, the identified vulnerability allows creating or deleting an object via the APIExport VirtualWorkspace in any arbitrary target workspace for pre-existing resources. By design, this should only be allowed when the workspace owner decides to give access to an API provider by creating an APIBinding. With this vulnerability, it is possible for an attacker to create and delete objects even if none of these requirements are satisfied, i.e. even if there is no APIBinding in that workspace at all or the workspace owner has created an APIBinding, but rejected a permission claim. A fix for this issue has been identified and has been published with kcp 0.26.3 and 0.27.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/kcp-dev/kcpGo
< 0.26.30.26.3

Patches

2
614ecbf35f11

Merge pull request #3338 from embik/binding-authorizer

https://github.com/kcp-dev/kcpkcp CI BotMar 18, 2025via ghsa
5 files changed · +403 10
  • pkg/virtual/apiexport/authorizer/binding.go+127 0 added
    @@ -0,0 +1,127 @@
    +/*
    +Copyright 2025 The KCP 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"
    +	"fmt"
    +	"slices"
    +	"strings"
    +
    +	"k8s.io/apimachinery/pkg/labels"
    +	"k8s.io/apiserver/pkg/authorization/authorizer"
    +	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
    +
    +	kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes"
    +	"github.com/kcp-dev/logicalcluster/v3"
    +
    +	dynamiccontext "github.com/kcp-dev/kcp/pkg/virtual/framework/dynamic/context"
    +	apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
    +	apisv1alpha1informers "github.com/kcp-dev/kcp/sdk/client/informers/externalversions/apis/v1alpha1"
    +)
    +
    +type boundAPIAuthorizer struct {
    +	getAPIBindingByExport func(clusterName, apiExportName, apiExportCluster string) (*apisv1alpha1.APIBinding, error)
    +
    +	delegate authorizer.Authorizer
    +}
    +
    +var readOnlyVerbs = []string{"get", "list", "watch"}
    +
    +func NewBoundAPIAuthorizer(delegate authorizer.Authorizer, apiBindingInformer apisv1alpha1informers.APIBindingClusterInformer, kubeClusterClient kcpkubernetesclientset.ClusterInterface) authorizer.Authorizer {
    +	apiBindingLister := apiBindingInformer.Lister()
    +
    +	return &boundAPIAuthorizer{
    +		delegate: delegate,
    +		getAPIBindingByExport: func(clusterName, apiExportName, apiExportCluster string) (*apisv1alpha1.APIBinding, error) {
    +			bindings, err := apiBindingLister.Cluster(logicalcluster.Name(clusterName)).List(labels.Everything())
    +			if err != nil {
    +				return nil, err
    +			}
    +
    +			for _, binding := range bindings {
    +				if binding == nil {
    +					continue
    +				}
    +
    +				if binding.Spec.Reference.Export != nil && binding.Spec.Reference.Export.Name == apiExportName && binding.Status.APIExportClusterName == apiExportCluster {
    +					return binding, nil
    +				}
    +			}
    +
    +			return nil, fmt.Errorf("no suitable binding found")
    +		},
    +	}
    +}
    +
    +func (a *boundAPIAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
    +	targetCluster, err := genericapirequest.ValidClusterFrom(ctx)
    +	if err != nil {
    +		return authorizer.DecisionNoOpinion, "", fmt.Errorf("error getting valid cluster from context: %w", err)
    +	}
    +
    +	if targetCluster.Wildcard || attr.GetResource() == "" {
    +		// if the target is the wildcard cluster or it's a non-resurce URL request,
    +		// we can skip checking the APIBinding in the target cluster.
    +		return a.delegate.Authorize(ctx, attr)
    +	}
    +
    +	apiDomainKey := dynamiccontext.APIDomainKeyFrom(ctx)
    +	parts := strings.Split(string(apiDomainKey), "/")
    +	if len(parts) < 2 {
    +		return authorizer.DecisionNoOpinion, "", fmt.Errorf("invalid API domain key")
    +	}
    +	apiExportCluster, apiExportName := parts[0], parts[1]
    +
    +	apiBinding, err := a.getAPIBindingByExport(targetCluster.Name.String(), apiExportName, apiExportCluster)
    +	if err != nil {
    +		return authorizer.DecisionDeny, "could not find suitable APIBinding in target logical cluster", nil //nolint:nilerr // this is on purpose, we want to deny, not return a server error
    +	}
    +
    +	// check if request is for a bound resource.
    +	for _, resource := range apiBinding.Status.BoundResources {
    +		if resource.Group == attr.GetAPIGroup() && resource.Resource == attr.GetResource() {
    +			return a.delegate.Authorize(ctx, attr)
    +		}
    +	}
    +
    +	// check if a resource claim for this resource has been accepted.
    +	for _, permissionClaim := range apiBinding.Spec.PermissionClaims {
    +		if permissionClaim.State != apisv1alpha1.ClaimAccepted {
    +			// if the claim is not accepted it cannot be used.
    +			continue
    +		}
    +
    +		if permissionClaim.Group == attr.GetAPIGroup() && permissionClaim.Resource == attr.GetResource() {
    +			return a.delegate.Authorize(ctx, attr)
    +		}
    +	}
    +
    +	// special case: APIBindings are always available from an APIExport VW,
    +	// but the provider should only be allowed to access them read-only to avoid privilege escalation.
    +	if attr.GetAPIGroup() == apisv1alpha1.SchemeGroupVersion.Group && attr.GetResource() == "apibindings" {
    +		if !slices.Contains(readOnlyVerbs, attr.GetVerb()) {
    +			return authorizer.DecisionNoOpinion, "write access to APIBinding is not allowed from virtual workspace", nil
    +		}
    +
    +		return a.delegate.Authorize(ctx, attr)
    +	}
    +
    +	// if we cannot find the API bound to the logical cluster, we deny.
    +	// The APIExport owner has not been invited in.
    +	return authorizer.DecisionDeny, "failed to find suitable reason to allow access in APIBinding", nil
    +}
    
  • pkg/virtual/apiexport/builder/build.go+8 4 modified
    @@ -70,7 +70,7 @@ func BuildVirtualWorkspace(
     	cfg *rest.Config,
     	kubeClusterClient, deepSARClient kcpkubernetesclientset.ClusterInterface,
     	kcpClusterClient kcpclientset.ClusterInterface,
    -	cachedKcpInformers kcpinformers.SharedInformerFactory,
    +	cachedKcpInformers, kcpInformers kcpinformers.SharedInformerFactory,
     ) ([]rootapiserver.NamedVirtualWorkspace, error) {
     	if !strings.HasSuffix(rootPathPrefix, "/") {
     		rootPathPrefix += "/"
    @@ -203,6 +203,7 @@ func BuildVirtualWorkspace(
     				for name, informer := range map[string]cache.SharedIndexInformer{
     					"apiresourceschemas": cachedKcpInformers.Apis().V1alpha1().APIResourceSchemas().Informer(),
     					"apiexports":         cachedKcpInformers.Apis().V1alpha1().APIExports().Informer(),
    +					"apibindings":        kcpInformers.Apis().V1alpha1().APIBindings().Informer(),
     				} {
     					if !cache.WaitForNamedCacheSync(name, hookContext.Done(), informer.HasSynced) {
     						klog.Background().Error(nil, "informer not synced")
    @@ -218,7 +219,7 @@ func BuildVirtualWorkspace(
     
     			return apiReconciler, nil
     		},
    -		Authorizer: newAuthorizer(kubeClusterClient, deepSARClient, cachedKcpInformers),
    +		Authorizer: newAuthorizer(kubeClusterClient, deepSARClient, cachedKcpInformers, kcpInformers),
     	}
     
     	return []rootapiserver.NamedVirtualWorkspace{
    @@ -291,14 +292,17 @@ func digestUrl(urlPath, rootPathPrefix string) (
     	return cluster, dynamiccontext.APIDomainKey(key), strings.TrimSuffix(urlPath, realPath), true
     }
     
    -func newAuthorizer(kubeClusterClient, deepSARClient kcpkubernetesclientset.ClusterInterface, cachedKcpInformers kcpinformers.SharedInformerFactory) authorizer.Authorizer {
    +func newAuthorizer(kubeClusterClient, deepSARClient kcpkubernetesclientset.ClusterInterface, cachedKcpInformers, kcpInformers kcpinformers.SharedInformerFactory) authorizer.Authorizer {
     	maximalPermissionAuth := virtualapiexportauth.NewMaximalPermissionAuthorizer(deepSARClient, cachedKcpInformers.Apis().V1alpha1().APIExports())
     	maximalPermissionAuth = authorization.NewDecorator("virtual.apiexport.maxpermissionpolicy.authorization.kcp.io", maximalPermissionAuth).AddAuditLogging().AddAnonymization().AddReasonAnnotation()
     
     	apiExportsContentAuth := virtualapiexportauth.NewAPIExportsContentAuthorizer(maximalPermissionAuth, kubeClusterClient)
     	apiExportsContentAuth = authorization.NewDecorator("virtual.apiexport.content.authorization.kcp.io", apiExportsContentAuth).AddAuditLogging().AddAnonymization()
     
    -	return apiExportsContentAuth
    +	boundApiAuth := virtualapiexportauth.NewBoundAPIAuthorizer(apiExportsContentAuth, kcpInformers.Apis().V1alpha1().APIBindings(), kubeClusterClient)
    +	boundApiAuth = authorization.NewDecorator("virtual.apiexport.boundapi.authorization.kcp.io", boundApiAuth).AddAuditLogging().AddAnonymization()
    +
    +	return boundApiAuth
     }
     
     // apiDefinitionWithCancel calls the cancelFn on tear-down.
    
  • pkg/virtual/apiexport/options/options.go+2 2 modified
    @@ -56,7 +56,7 @@ func (o *APIExport) Validate(flagPrefix string) []error {
     func (o *APIExport) NewVirtualWorkspaces(
     	rootPathPrefix string,
     	config *rest.Config,
    -	cachedKcpInformers kcpinformers.SharedInformerFactory,
    +	cachedKcpInformers, wildcardKcpInformers kcpinformers.SharedInformerFactory,
     ) (workspaces []rootapiserver.NamedVirtualWorkspace, err error) {
     	config = rest.AddUserAgent(rest.CopyConfig(config), "apiexport-virtual-workspace")
     	kcpClusterClient, err := kcpclientset.NewForConfig(config)
    @@ -72,5 +72,5 @@ func (o *APIExport) NewVirtualWorkspaces(
     		return nil, err
     	}
     
    -	return builder.BuildVirtualWorkspace(path.Join(rootPathPrefix, builder.VirtualWorkspaceName), config, kubeClusterClient, deepSARClient, kcpClusterClient, cachedKcpInformers)
    +	return builder.BuildVirtualWorkspace(path.Join(rootPathPrefix, builder.VirtualWorkspaceName), config, kubeClusterClient, deepSARClient, kcpClusterClient, cachedKcpInformers, wildcardKcpInformers)
     }
    
  • pkg/virtual/options/options.go+1 1 modified
    @@ -65,7 +65,7 @@ func (o *Options) NewVirtualWorkspaces(
     	wildcardKubeInformers kcpkubernetesinformers.SharedInformerFactory,
     	wildcardKcpInformers, cachedKcpInformers kcpinformers.SharedInformerFactory,
     ) ([]rootapiserver.NamedVirtualWorkspace, error) {
    -	apiexports, err := o.APIExport.NewVirtualWorkspaces(rootPathPrefix, config, cachedKcpInformers)
    +	apiexports, err := o.APIExport.NewVirtualWorkspaces(rootPathPrefix, config, cachedKcpInformers, wildcardKcpInformers)
     	if err != nil {
     		return nil, err
     	}
    
  • test/e2e/virtual/apiexport/authorizer_test.go+265 3 modified
    @@ -26,6 +26,7 @@ import (
     
     	"github.com/stretchr/testify/require"
     
    +	corev1 "k8s.io/api/core/v1"
     	rbacv1 "k8s.io/api/rbac/v1"
     	"k8s.io/apiextensions-apiserver/pkg/apihelpers"
     	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    @@ -465,11 +466,271 @@ metadata:
     		if err == nil {
     			return false, "expected error, got none"
     		}
    -		if apierrors.IsNotFound(err) {
    +		if apierrors.IsForbidden(err) {
     			return true, ""
     		}
    -		return false, fmt.Sprintf("expected a not-found error, but got %v", err)
    -	}, wait.ForeverTestTimeout, 100*time.Millisecond, "expected service-provider-2-admin to get a not-found for shadowed cowboy resources")
    +		return false, fmt.Sprintf("expected a forbidden error, but got %v", err)
    +	}, wait.ForeverTestTimeout, 100*time.Millisecond, "expected service-provider-2-admin to get a forbidden for shadowed cowboy resources")
    +}
    +
    +func TestAPIExportBindingAuthorizer(t *testing.T) {
    +	t.Parallel()
    +	framework.Suite(t, "control-plane")
    +
    +	server := kcptesting.SharedKcpServer(t)
    +
    +	ctx, cancel := context.WithCancel(context.Background())
    +	t.Cleanup(cancel)
    +
    +	orgPath, _ := framework.NewOrganizationFixture(t, server) //nolint:staticcheck
    +
    +	serviceProviderPath, _ := kcptesting.NewWorkspaceFixture(t, server, orgPath, kcptesting.WithName("service-provider"))
    +	tenantPath, tenantWorkspace := kcptesting.NewWorkspaceFixture(t, server, orgPath, kcptesting.WithName("tenant"))
    +
    +	cfg := server.BaseConfig(t)
    +
    +	serviceProviderAdmin := server.ClientCAUserConfig(t, rest.CopyConfig(cfg), "service-provider-admin")
    +	tenantUser := server.ClientCAUserConfig(t, rest.CopyConfig(cfg), "tenant-user")
    +
    +	kubeClient, err := kcpkubernetesclientset.NewForConfig(rest.CopyConfig(cfg))
    +	require.NoError(t, err)
    +	kcpClient, err := kcpclientset.NewForConfig(rest.CopyConfig(cfg))
    +	require.NoError(t, err)
    +
    +	framework.AdmitWorkspaceAccess(ctx, t, kubeClient, orgPath, []string{"service-provider-admin", "tenant-user"}, nil, false)
    +	framework.AdmitWorkspaceAccess(ctx, t, kubeClient, serviceProviderPath, []string{"service-provider-admin"}, nil, true)
    +	framework.AdmitWorkspaceAccess(ctx, t, kubeClient, tenantPath, []string{"tenant-user"}, nil, true)
    +
    +	t.Logf("install sherriffs API resource schema, API export, permissions for tenant-user to be able to bind to the export in service provider workspace %q", serviceProviderPath)
    +	require.NoError(t, apply(t, ctx, serviceProviderPath, serviceProviderAdmin,
    +		&apisv1alpha1.APIResourceSchema{
    +			ObjectMeta: metav1.ObjectMeta{Name: "today.sheriffs.wild.wild.west"},
    +			Spec: apisv1alpha1.APIResourceSchemaSpec{
    +				Group: "wild.wild.west",
    +				Names: apiextensionsv1.CustomResourceDefinitionNames{Plural: "sheriffs", Singular: "sheriff", Kind: "Sheriff", ListKind: "SheriffList"},
    +				Scope: "Namespaced",
    +				Versions: []apisv1alpha1.APIResourceVersion{
    +					{Name: "v1alpha1", Served: true, Storage: true, Schema: runtime.RawExtension{Raw: []byte(`{"type":"object"}`)}},
    +				},
    +			},
    +		},
    +		&apisv1alpha1.APIExport{
    +			ObjectMeta: metav1.ObjectMeta{Name: "wild.wild.west"},
    +			Spec: apisv1alpha1.APIExportSpec{
    +				LatestResourceSchemas: []string{"today.sheriffs.wild.wild.west"},
    +				PermissionClaims: []apisv1alpha1.PermissionClaim{
    +					{
    +						GroupResource: apisv1alpha1.GroupResource{
    +							Group:    "",
    +							Resource: "configmaps",
    +						},
    +						All: true,
    +					},
    +				},
    +			},
    +		},
    +
    +		&rbacv1.ClusterRole{
    +			ObjectMeta: metav1.ObjectMeta{Name: "tenant-user-bind-apiexport"},
    +			Rules: []rbacv1.PolicyRule{
    +				{APIGroups: []string{"apis.kcp.io"}, ResourceNames: []string{"wild.wild.west"}, Resources: []string{"apiexports"}, Verbs: []string{"bind"}},
    +			},
    +		},
    +		&rbacv1.ClusterRoleBinding{
    +			ObjectMeta: metav1.ObjectMeta{Name: "tenant-user-bind-apiexport"},
    +			Subjects:   []rbacv1.Subject{{Kind: "User", Name: "tenant-user"}},
    +			RoleRef:    rbacv1.RoleRef{APIGroup: rbacv1.SchemeGroupVersion.Group, Kind: "ClusterRole", Name: "tenant-user-bind-apiexport"},
    +		},
    +	))
    +
    +	t.Logf("Create virtual workspace client for \"today-sherriffs\" APIExport in workspace %q", serviceProviderPath)
    +	serviceProviderAdminApiExportVWCfg := rest.CopyConfig(serviceProviderAdmin)
    +	serviceProviderAdminClient, err := kcpclientset.NewForConfig(serviceProviderAdmin)
    +	require.NoError(t, err)
    +	kcptestinghelpers.Eventually(t, func() (bool, string) {
    +		apiExport, err := serviceProviderAdminClient.Cluster(serviceProviderPath).ApisV1alpha1().APIExports().Get(ctx, "wild.wild.west", metav1.GetOptions{})
    +		require.NoError(t, err)
    +		var found bool
    +		serviceProviderAdminApiExportVWCfg.Host, found, err = framework.VirtualWorkspaceURL(ctx, kcpClient, tenantWorkspace, framework.ExportVirtualWorkspaceURLs(apiExport))
    +		require.NoError(t, err)
    +		//nolint:staticcheck // SA1019 VirtualWorkspaces is deprecated but not removed yet
    +		return found, fmt.Sprintf("waiting for virtual workspace URLs to be available: %v", apiExport.Status.VirtualWorkspaces)
    +	}, wait.ForeverTestTimeout, time.Millisecond*100)
    +
    +	serviceProviderDynamicVWClientForTenantWorkspace, err := kcpdynamic.NewForConfig(serviceProviderAdminApiExportVWCfg)
    +	require.NoError(t, err)
    +
    +	configMap := &corev1.ConfigMap{
    +		TypeMeta: metav1.TypeMeta{
    +			Kind:       "ConfigMap",
    +			APIVersion: "v1",
    +		},
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name: "default",
    +		},
    +		Data: map[string]string{
    +			"a": "b",
    +		},
    +	}
    +	u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(configMap)
    +	require.NoError(t, err)
    +	cm := &unstructured.Unstructured{Object: u}
    +
    +	t.Logf("trying to create a ConfigMap in the tenant workspace before creating the APIBinding")
    +	_, err = serviceProviderDynamicVWClientForTenantWorkspace.Cluster(logicalcluster.NewPath(tenantWorkspace.Spec.Cluster)).
    +		Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps", Group: ""}).Namespace("default").Create(ctx, cm, metav1.CreateOptions{})
    +	require.True(t, apierrors.IsForbidden(err), "expected to be forbidden from creating ConfigMap")
    +
    +	t.Logf("trying to create a Sheriff in the tenant workspace before creating the APIBinding")
    +	_, err = serviceProviderDynamicVWClientForTenantWorkspace.Cluster(logicalcluster.NewPath(tenantWorkspace.Spec.Cluster)).
    +		Resource(schema.GroupVersionResource{Version: "v1", Resource: "sheriffs", Group: ""}).Namespace("default").Create(ctx, &unstructured.Unstructured{
    +		Object: map[string]any{
    +			"apiVersion": "wild.wild.west/v1alpha1",
    +			"kind":       "Sheriff",
    +			"metadata": map[string]any{
    +				"name": "default",
    +			},
    +		},
    +	}, metav1.CreateOptions{})
    +	require.True(t, apierrors.IsForbidden(err), "expected to be forbidden from creating Sheriff")
    +
    +	t.Logf("bind sherriffs with ConfigMaps PermissionClaim rejected in the tenant workspace %q", tenantPath)
    +	kcptestinghelpers.Eventually(t, func() (success bool, reason string) {
    +		err := apply(t, ctx, tenantPath, tenantUser,
    +			&apisv1alpha1.APIBinding{
    +				ObjectMeta: metav1.ObjectMeta{
    +					Name: "wild.wild.west",
    +				},
    +				Spec: apisv1alpha1.APIBindingSpec{
    +					PermissionClaims: []apisv1alpha1.AcceptablePermissionClaim{
    +						{
    +							PermissionClaim: apisv1alpha1.PermissionClaim{
    +								GroupResource: apisv1alpha1.GroupResource{Resource: "configmaps"},
    +								All:           true,
    +							},
    +							State: apisv1alpha1.ClaimRejected,
    +						},
    +					},
    +					Reference: apisv1alpha1.BindingReference{
    +						Export: &apisv1alpha1.ExportBindingReference{
    +							Path: serviceProviderPath.String(),
    +							Name: "wild.wild.west",
    +						},
    +					},
    +				},
    +			},
    +		)
    +		if err != nil {
    +			return false, err.Error()
    +		}
    +		return true, ""
    +	}, wait.ForeverTestTimeout, time.Millisecond*100)
    +
    +	t.Logf("trying to create a ConfigMap in the tenant workspace after creating the APIBinding with rejected PermissionClaim")
    +	_, err = serviceProviderDynamicVWClientForTenantWorkspace.Cluster(logicalcluster.NewPath(tenantWorkspace.Spec.Cluster)).
    +		Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps", Group: ""}).Namespace("default").Create(ctx, cm, metav1.CreateOptions{})
    +	require.True(t, apierrors.IsForbidden(err), "expected to be forbidden from creating ConfigMap")
    +
    +	t.Logf("update sherriffs APIBinding to accept the ConfigMaps PermissionClaim")
    +	kcptestinghelpers.Eventually(t, func() (success bool, reason string) {
    +		err := apply(t, ctx, tenantPath, tenantUser,
    +			&apisv1alpha1.APIBinding{
    +				ObjectMeta: metav1.ObjectMeta{
    +					Name: "wild.wild.west",
    +				},
    +				Spec: apisv1alpha1.APIBindingSpec{
    +					PermissionClaims: []apisv1alpha1.AcceptablePermissionClaim{
    +						{
    +							PermissionClaim: apisv1alpha1.PermissionClaim{
    +								GroupResource: apisv1alpha1.GroupResource{Resource: "configmaps"},
    +								All:           true,
    +							},
    +							State: apisv1alpha1.ClaimAccepted,
    +						},
    +					},
    +					Reference: apisv1alpha1.BindingReference{
    +						Export: &apisv1alpha1.ExportBindingReference{
    +							Path: serviceProviderPath.String(),
    +							Name: "wild.wild.west",
    +						},
    +					},
    +				},
    +			},
    +		)
    +		if err != nil {
    +			return false, err.Error()
    +		}
    +		return true, ""
    +	}, wait.ForeverTestTimeout, time.Millisecond*100)
    +
    +	t.Logf("trying to create a ConfigMap in the tenant workspace after updating the APIBinding with accepted PermissionClaim")
    +	kcptestinghelpers.Eventually(t, func() (bool, string) {
    +		_, err = serviceProviderDynamicVWClientForTenantWorkspace.Cluster(logicalcluster.NewPath(tenantWorkspace.Spec.Cluster)).
    +			Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps", Group: ""}).Namespace("default").Create(ctx, cm, metav1.CreateOptions{})
    +		if err != nil {
    +			return false, err.Error()
    +		}
    +
    +		return true, ""
    +	}, wait.ForeverTestTimeout, 100*time.Millisecond, "error creating ConfigMap")
    +
    +	t.Logf("trying to create a Sheriff in the tenant workspace")
    +	kcptestinghelpers.Eventually(t, func() (bool, string) {
    +		_, err = serviceProviderDynamicVWClientForTenantWorkspace.Cluster(logicalcluster.NewPath(tenantWorkspace.Spec.Cluster)).
    +			Resource(schema.GroupVersionResource{Version: "v1alpha1", Resource: "sheriffs", Group: "wild.wild.west"}).Namespace("default").Create(ctx, &unstructured.Unstructured{
    +			Object: map[string]any{
    +				"apiVersion": "wild.wild.west/v1alpha1",
    +				"kind":       "Sheriff",
    +				"metadata": map[string]any{
    +					"name": "default",
    +				},
    +			},
    +		}, metav1.CreateOptions{})
    +		if err != nil {
    +			return false, err.Error()
    +		}
    +		return true, ""
    +	}, wait.ForeverTestTimeout, 100*time.Millisecond, "error creating Sheriff")
    +
    +	t.Logf("update sherriffs APIBinding to reject the ConfigMaps PermissionClaim again")
    +	kcptestinghelpers.Eventually(t, func() (success bool, reason string) {
    +		err := apply(t, ctx, tenantPath, tenantUser,
    +			&apisv1alpha1.APIBinding{
    +				ObjectMeta: metav1.ObjectMeta{
    +					Name: "wild.wild.west",
    +				},
    +				Spec: apisv1alpha1.APIBindingSpec{
    +					PermissionClaims: []apisv1alpha1.AcceptablePermissionClaim{
    +						{
    +							PermissionClaim: apisv1alpha1.PermissionClaim{
    +								GroupResource: apisv1alpha1.GroupResource{Resource: "configmaps"},
    +								All:           true,
    +							},
    +							State: apisv1alpha1.ClaimRejected,
    +						},
    +					},
    +					Reference: apisv1alpha1.BindingReference{
    +						Export: &apisv1alpha1.ExportBindingReference{
    +							Path: serviceProviderPath.String(),
    +							Name: "wild.wild.west",
    +						},
    +					},
    +				},
    +			},
    +		)
    +		if err != nil {
    +			return false, err.Error()
    +		}
    +		return true, ""
    +	}, wait.ForeverTestTimeout, time.Millisecond*100)
    +
    +	// I know, I know, but we need to wait for the APIBinding update to reach the informer in the authorizer ...
    +	time.Sleep(5 * time.Second)
    +
    +	t.Logf("trying to delete the created ConfigMap in the tenant workspace after updating the APIBinding with rejected PermissionClaim")
    +	err = serviceProviderDynamicVWClientForTenantWorkspace.Cluster(logicalcluster.NewPath(tenantWorkspace.Spec.Cluster)).
    +		Resource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps", Group: ""}).Namespace("default").Delete(ctx, "default", metav1.DeleteOptions{})
    +	require.True(t, apierrors.IsForbidden(err), "expected to be forbidden from deleting ConfigMap")
     }
     
     var scheme *runtime.Scheme
    @@ -479,6 +740,7 @@ func init() {
     	_ = apisv1alpha1.AddToScheme(scheme)
     	_ = rbacv1.AddToScheme(scheme)
     	_ = apiextensionsv1.AddToScheme(scheme)
    +	_ = corev1.AddToScheme(scheme)
     }
     
     func apply(t *testing.T, ctx context.Context, workspace logicalcluster.Path, cfg *rest.Config, manifests ...any) error {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.