From cc45bdd949280dc4da9ec38a554c4e8bf3ef037f Mon Sep 17 00:00:00 2001 From: olalekan odukoya Date: Mon, 8 Jun 2026 02:28:54 +0100 Subject: [PATCH] validate APIServiceExportRequest permission claims via CEL Signed-off-by: olalekan odukoya --- .../serviceexportrequest_reconcile.go | 53 +------------------ .../resources/apiexport-kube-bind.io.yaml | 2 +- ...apiserviceexportrequests.kube-bind.io.yaml | 6 ++- ...kube-bind.io_apiserviceexportrequests.yaml | 4 ++ ...kube-bind.io_apiserviceexportrequests.yaml | 4 ++ .../v1alpha2/apiserviceexportrequest_types.go | 5 +- 6 files changed, 19 insertions(+), 55 deletions(-) diff --git a/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go b/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go index 42cebc800..ebe184b02 100644 --- a/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go +++ b/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go @@ -319,11 +319,7 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, existi } // Validate validates if the APIServiceExportRequest is in a valid state. -// Currently it validates if all requested schemas are of the same scope and -// if claimable apis are allowed and valid. -// -// TODO: Move this to validatingAdmissionWebhook as this is not really part of reconciliation. -// https://github.com/kube-bind/kube-bind/issues/325 +// Currently it validates if all requested schemas are of the same scope. func (r *reconciler) validate(ctx context.Context, cl client.Client, req *kubebindv1alpha2.APIServiceExportRequest) error { exportedSchemas, err := r.getExportedSchemas(ctx, cl) if err != nil { @@ -371,56 +367,9 @@ func (r *reconciler) validate(ctx context.Context, cl client.Client, req *kubebi } } - // Add validation if claimable apis are valid here - for _, claim := range req.Spec.PermissionClaims { - if !isClaimableAPI(claim) { - conditions.MarkFalse( - req, - kubebindv1alpha2.APIServiceExportConditionPermissionClaim, - "InvalidPermissionClaim", - conditionsapi.ConditionSeverityError, - "Resource %s is not a valid claimable API", - claim.GroupResource.String(), - ) - req.Status.Phase = kubebindv1alpha2.APIServiceExportRequestPhaseFailed - req.Status.TerminalMessage = conditions.GetMessage(req, kubebindv1alpha2.APIServiceExportConditionPermissionClaim) - return fmt.Errorf("resource %s is not a valid claimable API", claim.GroupResource.String()) - } - } - - // Add validation for duplicate group/resource combinations - seenGroupResources := make(map[string]bool) - for _, claim := range req.Spec.PermissionClaims { - key := claim.Group + "/" + claim.Resource - if seenGroupResources[key] { - conditions.MarkFalse( - req, - kubebindv1alpha2.APIServiceExportConditionPermissionClaim, - "DuplicatePermissionClaim", - conditionsapi.ConditionSeverityError, - "Duplicate permission claim found for group/resource %s", - claim.GroupResource.String(), - ) - req.Status.Phase = kubebindv1alpha2.APIServiceExportRequestPhaseFailed - req.Status.TerminalMessage = conditions.GetMessage(req, kubebindv1alpha2.APIServiceExportConditionPermissionClaim) - return fmt.Errorf("duplicate permission claim found for group/resource %s", claim.GroupResource.String()) - } - seenGroupResources[key] = true - } - return nil } -// isClaimableAPI checks if a permission claim is for a claimable API. -func isClaimableAPI(claim kubebindv1alpha2.PermissionClaim) bool { - for _, api := range kubebindv1alpha2.ClaimableAPIs { - if claim.Group == api.GroupVersionResource.Group && claim.Resource == api.Names.Plural { - return true - } - } - return false -} - func (r *reconciler) ensureAPIServiceNamespaces(ctx context.Context, cl client.Client, cache cache.Cache, req *kubebindv1alpha2.APIServiceExportRequest) error { logger := klog.FromContext(ctx) diff --git a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml index ab9e3d02b..f0be0887a 100644 --- a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml @@ -66,7 +66,7 @@ spec: crd: {} - group: kube-bind.io name: apiserviceexportrequests - schema: v260220-f03e1ee.apiserviceexportrequests.kube-bind.io + schema: v260608-edcd0b51.apiserviceexportrequests.kube-bind.io storage: crd: {} - group: kube-bind.io diff --git a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml index a9c4abdfb..dcfd2b91a 100644 --- a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexportrequests.kube-bind.io.yaml @@ -1,7 +1,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: - name: v260220-f03e1ee.apiserviceexportrequests.kube-bind.io + name: v260608-edcd0b51.apiserviceexportrequests.kube-bind.io spec: conversion: strategy: None @@ -384,6 +384,10 @@ spec: x-kubernetes-validations: - message: permissionClaims are immutable rule: self == oldSelf + - message: Resource is not a valid claimable API + rule: self.all(c, (c.group == '' && c.resource == 'configmaps') || + (c.group == '' && c.resource == 'secrets') || (c.group == '' && + c.resource == 'serviceaccounts')) resources: description: resources is a list of resources that should be exported. items: diff --git a/deploy/charts/backend/crds/kube-bind.io_apiserviceexportrequests.yaml b/deploy/charts/backend/crds/kube-bind.io_apiserviceexportrequests.yaml index d5d06e244..8a742e410 100644 --- a/deploy/charts/backend/crds/kube-bind.io_apiserviceexportrequests.yaml +++ b/deploy/charts/backend/crds/kube-bind.io_apiserviceexportrequests.yaml @@ -388,6 +388,10 @@ spec: x-kubernetes-validations: - message: permissionClaims are immutable rule: self == oldSelf + - message: Resource is not a valid claimable API + rule: self.all(c, (c.group == '' && c.resource == 'configmaps') + || (c.group == '' && c.resource == 'secrets') || (c.group == '' + && c.resource == 'serviceaccounts')) resources: description: resources is a list of resources that should be exported. items: diff --git a/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml b/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml index 22ecb0a05..a422b83a3 100644 --- a/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexportrequests.yaml @@ -389,6 +389,10 @@ spec: x-kubernetes-validations: - message: permissionClaims are immutable rule: self == oldSelf + - message: Resource is not a valid claimable API + rule: self.all(c, (c.group == '' && c.resource == 'configmaps') + || (c.group == '' && c.resource == 'secrets') || (c.group == '' + && c.resource == 'serviceaccounts')) resources: description: resources is a list of resources that should be exported. items: diff --git a/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go b/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go index a02abaeb6..4a1b1cdd2 100644 --- a/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiserviceexportrequest_types.go @@ -114,8 +114,11 @@ type APIServiceExportRequestSpec struct { // Access is granted per GroupResource. // // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="permissionClaims are immutable" + // +kubebuilder:validation:XListType=map + // +kubebuilder:validation:XListMapKey=resource + // +kubebuilder:validation:XListMapKey=group + // +kubebuilder:validation:XValidation:rule="self.all(c, (c.group == '' && c.resource == 'configmaps') || (c.group == '' && c.resource == 'secrets') || (c.group == '' && c.resource == 'serviceaccounts'))",message="Resource is not a valid claimable API" PermissionClaims []PermissionClaim `json:"permissionClaims,omitempty"` - // namespaces specifies the namespaces to bootstrap as part of this request. // When objects originate from provider side, the consumer does not always know the necessary details. // This field allows provider to pre-heat the necessary namespaces on provider side by creating