This is an automated email from the ASF dual-hosted git repository.

pcongiusti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git


The following commit(s) were added to refs/heads/main by this push:
     new b262ccc48 chore(ctrl): check SA permission to access cross namespace 
resources
b262ccc48 is described below

commit b262ccc4853546b7ea5ec4b24555e7c4c65406a1
Author: Pasquale Congiusti <[email protected]>
AuthorDate: Sat Nov 15 08:59:50 2025 +0100

    chore(ctrl): check SA permission to access cross namespace resources
    
    We make sure that the pipe/integration SA is authorized to access to the 
resources in other namespaces before binding.
---
 .../cross-ns}/my-timer-source-ns.kamelet.yaml      |  0
 .../cross-ns/pipe-cross-ns.yaml}                   | 29 +++----
 .../cross-ns/sa-role.yaml}                         | 27 ++-----
 .../cross-ns/sa-rolebinding.yaml}                  | 31 +++-----
 .../cross-ns/sa.yaml}                              | 23 +-----
 e2e/common/misc/pipe_cross_ns_test.go              | 91 ++++++++++++++++++++++
 .../my-timer-source-ns.kamelet.yaml                |  0
 .../sa-role.yaml}                                  | 27 ++-----
 .../sa-rolebinding.yaml}                           | 31 +++-----
 .../sa.yaml}                                       | 23 +-----
 e2e/common/traits/kamelet_test.go                  | 17 +++-
 .../integration/integration_controller.go          |  2 +-
 pkg/controller/pipe/integration.go                 | 11 +--
 pkg/controller/synthetic/synthetic.go              |  2 +-
 pkg/install/openshift.go                           |  2 +-
 pkg/internal/client.go                             | 20 +++++
 .../rbac/descoped/operator-cluster-role.yaml       |  7 ++
 pkg/trait/kamelets.go                              | 46 +++++++++--
 pkg/trait/kamelets_test.go                         | 26 +++++--
 pkg/util/bindings/api.go                           | 11 +--
 pkg/util/bindings/catalog.go                       | 34 ++++++++
 pkg/util/bindings/catalog_test.go                  | 67 +++++++++++++++-
 pkg/util/kubernetes/permission.go                  | 30 ++++++-
 23 files changed, 391 insertions(+), 166 deletions(-)

diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/misc/cross-ns/my-timer-source-ns.kamelet.yaml
similarity index 100%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/my-timer-source-ns.kamelet.yaml
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/misc/cross-ns/pipe-cross-ns.yaml
similarity index 71%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/pipe-cross-ns.yaml
index dca31ad89..13e31be8f 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/misc/cross-ns/pipe-cross-ns.yaml
@@ -14,23 +14,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ---------------------------------------------------------------------------
+
 apiVersion: camel.apache.org/v1
-kind: Kamelet
+kind: Pipe
 metadata:
-  name: my-timer-source
-  labels:
-    camel.apache.org/kamelet.type: "source"
+  name: pipe-cross-ns
 spec:
-  definition:
-    title: "Timer Example"
-    description: "Produces periodic events with a custom payload"
-  types:
-    out:
-      mediaType: text/plain
-  template:
-    from:
-      uri: timer:tick
-      steps:
-        - setBody:
-            constant: "Kamelet NS"
-        - to: "kamelet:sink"
+  serviceAccountName: my-cross-ns-sa
+  sink:
+    uri: log:bye
+  source:
+    ref:
+      apiVersion: camel.apache.org/v1
+      kind: Kamelet
+      name: my-timer-source
+      namespace: %%%
\ No newline at end of file
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/misc/cross-ns/sa-role.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/sa-role.yaml
index dca31ad89..60787d812 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/misc/cross-ns/sa-role.yaml
@@ -14,23 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
 metadata:
-  name: my-timer-source
-  labels:
-    camel.apache.org/kamelet.type: "source"
-spec:
-  definition:
-    title: "Timer Example"
-    description: "Produces periodic events with a custom payload"
-  types:
-    out:
-      mediaType: text/plain
-  template:
-    from:
-      uri: timer:tick
-      steps:
-        - setBody:
-            constant: "Kamelet NS"
-        - to: "kamelet:sink"
+  name: test-get-kamelets
+rules:
+- apiGroups: ["camel.apache.org"]
+  resources: ["kamelets"]
+  verbs: ["get"]
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/misc/cross-ns/sa-rolebinding.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/sa-rolebinding.yaml
index dca31ad89..0dc04e183 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/misc/cross-ns/sa-rolebinding.yaml
@@ -14,23 +14,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
 metadata:
-  name: my-timer-source
-  labels:
-    camel.apache.org/kamelet.type: "source"
-spec:
-  definition:
-    title: "Timer Example"
-    description: "Produces periodic events with a custom payload"
-  types:
-    out:
-      mediaType: text/plain
-  template:
-    from:
-      uri: timer:tick
-      steps:
-        - setBody:
-            constant: "Kamelet NS"
-        - to: "kamelet:sink"
+  name: sa-get-kamelets
+subjects:
+- kind: ServiceAccount
+  name: my-cross-ns-sa
+  namespace: %%%
+roleRef:
+  kind: Role
+  name: test-get-kamelets
+  apiGroup: rbac.authorization.k8s.io
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/misc/cross-ns/sa.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/misc/cross-ns/sa.yaml
index dca31ad89..9d6e9b0d3 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/misc/cross-ns/sa.yaml
@@ -14,23 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: v1
+kind: ServiceAccount
 metadata:
-  name: my-timer-source
-  labels:
-    camel.apache.org/kamelet.type: "source"
-spec:
-  definition:
-    title: "Timer Example"
-    description: "Produces periodic events with a custom payload"
-  types:
-    out:
-      mediaType: text/plain
-  template:
-    from:
-      uri: timer:tick
-      steps:
-        - setBody:
-            constant: "Kamelet NS"
-        - to: "kamelet:sink"
+  name: my-cross-ns-sa
diff --git a/e2e/common/misc/pipe_cross_ns_test.go 
b/e2e/common/misc/pipe_cross_ns_test.go
new file mode 100644
index 000000000..8d8821dd2
--- /dev/null
+++ b/e2e/common/misc/pipe_cross_ns_test.go
@@ -0,0 +1,91 @@
+//go:build integration
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> 
Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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 common
+
+import (
+       "context"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+
+       . "github.com/onsi/gomega"
+       corev1 "k8s.io/api/core/v1"
+
+       . "github.com/apache/camel-k/v2/e2e/support"
+       v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+)
+
+func TestPipeCrossNamespaceKamelet(t *testing.T) {
+       t.Parallel()
+       WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns1 string) 
{
+               t.Run("store kamelet", func(t *testing.T) {
+                       ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
"cross-ns/my-timer-source-ns.kamelet.yaml", "-n", ns1))
+               })
+
+               WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns2 
string) {
+                       t.Run("set privileges", func(t *testing.T) {
+                               ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
"cross-ns/sa.yaml", "-n", ns2))
+                               ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
"cross-ns/sa-role.yaml", "-n", ns1))
+                               saRbFile := cloneAndReplaceNamespace(t, 
"cross-ns/sa-rolebinding.yaml", ns2)
+                               ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
saRbFile, "-n", ns1))
+                       })
+                       t.Run("cross namespace pipe", func(t *testing.T) {
+                               // Clone the resource in a temporary file as it 
will require to be changed
+                               pipeFile := cloneAndReplaceNamespace(t, 
"cross-ns/pipe-cross-ns.yaml", ns1)
+                               name := "pipe-cross-ns"
+                               ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
pipeFile, "-n", ns2))
+                               g.Eventually(IntegrationConditionStatus(t, ctx, 
ns2, name, v1.IntegrationConditionReady), TestTimeoutMedium).
+                                       Should(Equal(corev1.ConditionTrue))
+                               g.Eventually(IntegrationPodPhase(t, ctx, ns2, 
name), TestTimeoutShort).Should(Equal(corev1.PodRunning))
+                               g.Eventually(IntegrationLogs(t, ctx, ns2, 
name), TestTimeoutShort).Should(ContainSubstring("Kamelet NS"))
+                       })
+               })
+       })
+}
+
+// cloneAndReplaceNamespace clones and replace the content marked as %%% with 
the namespace passed as parameter.
+func cloneAndReplaceNamespace(t *testing.T, srcPath, namespace string) string {
+       t.Helper()
+
+       tempDir := t.TempDir()
+       tempPath := filepath.Join(tempDir, filepath.Base(srcPath))
+
+       dstFile, err := os.Create(tempPath)
+       if err != nil {
+               t.Fatalf("failed to create temp file: %v", err)
+       }
+       defer dstFile.Close()
+
+       content, err := os.ReadFile(srcPath)
+       if err != nil {
+               t.Fatalf("failed to read src file: %v", err)
+       }
+       updated := strings.ReplaceAll(string(content), "%%%", namespace)
+       err = os.WriteFile(tempPath, []byte(updated), 0644)
+       if err != nil {
+               t.Fatalf("failed to write dst file: %v", err)
+       }
+
+       return tempPath
+}
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/traits/cross-ns/my-timer-source-ns.kamelet.yaml
similarity index 100%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/traits/cross-ns/my-timer-source-ns.kamelet.yaml
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/traits/cross-ns/sa-role.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/traits/cross-ns/sa-role.yaml
index dca31ad89..60787d812 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/traits/cross-ns/sa-role.yaml
@@ -14,23 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
 metadata:
-  name: my-timer-source
-  labels:
-    camel.apache.org/kamelet.type: "source"
-spec:
-  definition:
-    title: "Timer Example"
-    description: "Produces periodic events with a custom payload"
-  types:
-    out:
-      mediaType: text/plain
-  template:
-    from:
-      uri: timer:tick
-      steps:
-        - setBody:
-            constant: "Kamelet NS"
-        - to: "kamelet:sink"
+  name: test-get-kamelets
+rules:
+- apiGroups: ["camel.apache.org"]
+  resources: ["kamelets"]
+  verbs: ["get"]
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/traits/cross-ns/sa-rolebinding.yaml
similarity index 69%
copy from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
copy to e2e/common/traits/cross-ns/sa-rolebinding.yaml
index dca31ad89..0dc04e183 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/traits/cross-ns/sa-rolebinding.yaml
@@ -14,23 +14,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
 metadata:
-  name: my-timer-source
-  labels:
-    camel.apache.org/kamelet.type: "source"
-spec:
-  definition:
-    title: "Timer Example"
-    description: "Produces periodic events with a custom payload"
-  types:
-    out:
-      mediaType: text/plain
-  template:
-    from:
-      uri: timer:tick
-      steps:
-        - setBody:
-            constant: "Kamelet NS"
-        - to: "kamelet:sink"
+  name: sa-get-kamelets
+subjects:
+- kind: ServiceAccount
+  name: my-cross-ns-sa
+  namespace: %%%
+roleRef:
+  kind: Role
+  name: test-get-kamelets
+  apiGroup: rbac.authorization.k8s.io
diff --git a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml 
b/e2e/common/traits/cross-ns/sa.yaml
similarity index 69%
rename from e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
rename to e2e/common/traits/cross-ns/sa.yaml
index dca31ad89..9d6e9b0d3 100644
--- a/e2e/common/traits/files/my-timer-source-ns.kamelet.yaml
+++ b/e2e/common/traits/cross-ns/sa.yaml
@@ -14,23 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ---------------------------------------------------------------------------
-apiVersion: camel.apache.org/v1
-kind: Kamelet
+
+apiVersion: v1
+kind: ServiceAccount
 metadata:
-  name: my-timer-source
-  labels:
-    camel.apache.org/kamelet.type: "source"
-spec:
-  definition:
-    title: "Timer Example"
-    description: "Produces periodic events with a custom payload"
-  types:
-    out:
-      mediaType: text/plain
-  template:
-    from:
-      uri: timer:tick
-      steps:
-        - setBody:
-            constant: "Kamelet NS"
-        - to: "kamelet:sink"
+  name: my-cross-ns-sa
diff --git a/e2e/common/traits/kamelet_test.go 
b/e2e/common/traits/kamelet_test.go
index 1dedf9c7e..9fa431969 100644
--- a/e2e/common/traits/kamelet_test.go
+++ b/e2e/common/traits/kamelet_test.go
@@ -89,15 +89,24 @@ func TestKameletNamespaced(t *testing.T) {
        t.Parallel()
        WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns1 string) 
{
                t.Run("store kamelet", func(t *testing.T) {
-                       ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
"files/my-timer-source-ns.kamelet.yaml", "-n", ns1))
+                       ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
"cross-ns/my-timer-source-ns.kamelet.yaml", "-n", ns1))
                })
 
                WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns2 
string) {
-                       t.Run("namespaced kamelet", func(t *testing.T) {
+                       t.Run("set privileges", func(t *testing.T) {
+                               ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
"cross-ns/sa.yaml", "-n", ns2))
+                               ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
"cross-ns/sa-role.yaml", "-n", ns1))
+                               saRbFile := cloneAndReplaceNamespace(t, 
"cross-ns/sa-rolebinding.yaml", ns2)
+                               ExpectExecSucceed(t, g, Kubectl("apply", "-f", 
saRbFile, "-n", ns1))
+                       })
+                       t.Run("cross namespace kamelet", func(t *testing.T) {
                                // Clone the resource in a temporary file as it 
will require to be changed
                                routeFile := cloneAndReplaceNamespace(t, 
"files/kamelet-it-ns.yaml", ns1)
-                               name := 
RandomizedSuffixName("namespaced-kamelet")
-                               g.Expect(KamelRun(t, ctx, ns2, routeFile, 
"--name", name).Execute()).To(Succeed())
+                               name := 
RandomizedSuffixName("cross-namespace-kamelet")
+                               g.Expect(KamelRun(t, ctx, ns2, routeFile,
+                                       "--service-account", "my-cross-ns-sa",
+                                       "--name", name,
+                               ).Execute()).To(Succeed())
                                g.Eventually(IntegrationConditionStatus(t, ctx, 
ns2, name, v1.IntegrationConditionReady), TestTimeoutMedium).
                                        Should(Equal(corev1.ConditionTrue))
                                g.Eventually(IntegrationPodPhase(t, ctx, ns2, 
name), TestTimeoutShort).Should(Equal(corev1.PodRunning))
diff --git a/pkg/controller/integration/integration_controller.go 
b/pkg/controller/integration/integration_controller.go
index 4d6e1548c..28b3ed861 100644
--- a/pkg/controller/integration/integration_controller.go
+++ b/pkg/controller/integration/integration_controller.go
@@ -412,7 +412,7 @@ func watchKnativeResources(ctx context.Context, c 
client.Client, b *builder.Buil
        // Check for permission to watch the Knative Service resource
        checkCtx, cancel := context.WithTimeout(ctx, time.Minute)
        defer cancel()
-       if ok, err = kubernetes.CheckPermission(checkCtx, c, serving.GroupName, 
"services", platform.GetOperatorWatchNamespace(), "", "watch"); err != nil {
+       if ok, err = kubernetes.CheckSelfPermission(checkCtx, c, 
serving.GroupName, "services", platform.GetOperatorWatchNamespace(), "", 
"watch"); err != nil {
                return err
        } else if ok {
                log.Info("KnativeService resources installed in the cluster. 
RBAC privileges assigned correctly, you can use Knative features.")
diff --git a/pkg/controller/pipe/integration.go 
b/pkg/controller/pipe/integration.go
index ffe3d5cbc..f31fbdab2 100644
--- a/pkg/controller/pipe/integration.go
+++ b/pkg/controller/pipe/integration.go
@@ -110,11 +110,12 @@ func CreateIntegrationFor(ctx context.Context, c 
client.Client, pipe *v1.Pipe) (
        }
 
        bindingContext := bindings.BindingContext{
-               Ctx:       ctx,
-               Client:    c,
-               Namespace: it.Namespace,
-               Profile:   profile,
-               Metadata:  it.Annotations,
+               Ctx:                ctx,
+               Client:             c,
+               Namespace:          it.Namespace,
+               Profile:            profile,
+               Metadata:           it.Annotations,
+               ServiceAccountName: it.Spec.ServiceAccountName,
        }
 
        from, err := bindings.Translate(bindingContext, 
endpointTypeSourceContext, pipe.Spec.Source)
diff --git a/pkg/controller/synthetic/synthetic.go 
b/pkg/controller/synthetic/synthetic.go
index 286eea16a..a3673e4b2 100644
--- a/pkg/controller/synthetic/synthetic.go
+++ b/pkg/controller/synthetic/synthetic.go
@@ -141,7 +141,7 @@ func getInformers(ctx context.Context, cl client.Client, c 
cache.Cache) ([]cache
        }
        // Watch for the Knative Services conditionally
        if ok, err := kubernetes.IsAPIResourceInstalled(cl, 
servingv1.SchemeGroupVersion.String(), 
reflect.TypeOf(servingv1.Service{}).Name()); ok && err == nil {
-               if ok, err := kubernetes.CheckPermission(ctx, cl, 
serving.GroupName, "services", platform.GetOperatorWatchNamespace(), "", 
"watch"); ok && err == nil {
+               if ok, err := kubernetes.CheckSelfPermission(ctx, cl, 
serving.GroupName, "services", platform.GetOperatorWatchNamespace(), "", 
"watch"); ok && err == nil {
                        ksvc, err := c.GetInformer(ctx, &servingv1.Service{})
                        if err != nil {
                                return nil, err
diff --git a/pkg/install/openshift.go b/pkg/install/openshift.go
index 0d72152b7..26a4484c3 100644
--- a/pkg/install/openshift.go
+++ b/pkg/install/openshift.go
@@ -69,7 +69,7 @@ func OpenShiftConsoleDownloadLink(ctx context.Context, c 
client.Client) error {
        }
 
        // Check for permission to create the ConsoleCLIDownload resource
-       ok, err = kubernetes.CheckPermission(ctx, c, console.GroupName, 
"consoleclidownloads", "", KamelCLIDownloadName, "create")
+       ok, err = kubernetes.CheckSelfPermission(ctx, c, console.GroupName, 
"consoleclidownloads", "", KamelCLIDownloadName, "create")
        if err != nil {
                return err
        }
diff --git a/pkg/internal/client.go b/pkg/internal/client.go
index acc9cd2c9..8bfb1a3ef 100644
--- a/pkg/internal/client.go
+++ b/pkg/internal/client.go
@@ -28,6 +28,7 @@ import (
        fakecamelclientset 
"github.com/apache/camel-k/v2/pkg/client/camel/clientset/versioned/fake"
        camelv1 
"github.com/apache/camel-k/v2/pkg/client/camel/clientset/versioned/typed/camel/v1"
        "github.com/apache/camel-k/v2/pkg/util"
+       authv1 "k8s.io/api/authorization/v1"
        autoscalingv1 "k8s.io/api/autoscaling/v1"
        corev1 "k8s.io/api/core/v1"
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
@@ -249,6 +250,25 @@ func (f *FakeAuthorization) SelfSubjectRulesReviews() 
authorizationv1.SelfSubjec
        return f.AuthorizationV1Interface.SelfSubjectRulesReviews()
 }
 
+// Returns a fake SAR interface.
+func (f *FakeAuthorization) SubjectAccessReviews() 
authorizationv1.SubjectAccessReviewInterface {
+       return &FakeSAR{}
+}
+
+type FakeSAR struct {
+       authorizationv1.SubjectAccessReviewInterface
+}
+
+// Fake Create implementation (needed in cross namespace Kamelets test). Only 
allow `cross-ns-sa` user in `default` namespace.
+func (f *FakeSAR) Create(ctx context.Context, sar *authv1.SubjectAccessReview, 
opts metav1.CreateOptions) (*authv1.SubjectAccessReview, error) {
+       ra := sar.Spec.ResourceAttributes
+       allowed := sar.Spec.User == "system:serviceaccount:default:cross-ns-sa" 
&& ra.Verb == "get" && ra.Resource == "kamelets"
+
+       sar.Status.Allowed = allowed
+       sar.Status.Reason = "mocked"
+       return sar, nil
+}
+
 type FakeDiscovery struct {
        discovery.DiscoveryInterface
 
diff --git a/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml 
b/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
index 1c2e4bb4a..dc20bfb86 100644
--- a/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
+++ b/pkg/resources/config/rbac/descoped/operator-cluster-role.yaml
@@ -183,3 +183,10 @@ rules:
   verbs:
   - get
   - list
+# Required to check if a ServiceAccount can access other namespaces resources
+- apiGroups:
+  - authorization.k8s.io
+  resources:
+  - subjectaccessreviews
+  verbs:
+  - create
\ No newline at end of file
diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go
index 860dd5bd8..dfcf86cdb 100644
--- a/pkg/trait/kamelets.go
+++ b/pkg/trait/kamelets.go
@@ -39,6 +39,7 @@ import (
        "github.com/apache/camel-k/v2/pkg/util/camel"
        "github.com/apache/camel-k/v2/pkg/util/digest"
        "github.com/apache/camel-k/v2/pkg/util/dsl"
+       "github.com/apache/camel-k/v2/pkg/util/kubernetes"
 )
 
 const (
@@ -101,7 +102,7 @@ func (t *kameletsTrait) Apply(e *Environment) error {
 
 // collectKamelets load a Kamelet specification setting the specific version 
specification.
 func (t *kameletsTrait) collectKamelets(e *Environment) 
(map[string]*v1.Kamelet, error) {
-       namespaces, err := t.calculateNamespaces(e.Integration.Namespace, 
platform.GetOperatorNamespace())
+       namespaces, err := t.calculateNamespaces(e, e.Integration.Namespace, 
platform.GetOperatorNamespace())
        if err != nil {
                return nil, err
        }
@@ -167,7 +168,8 @@ func (t *kameletsTrait) collectKamelets(e *Environment) 
(map[string]*v1.Kamelet,
        kameletsAvailabilityMessage := ""
        cond := corev1.ConditionTrue
        if len(missingKamelets) > 0 {
-               kameletsAvailabilityMessage = fmt.Sprintf("Kamelets [%s] not 
found in cluster. Make sure to include the Kamelets dependency in the 
Integration.",
+               kameletsAvailabilityMessage = fmt.Sprintf("Kamelets [%s] not 
found in cluster. "+
+                       "Make sure to include the Kamelets dependency in the 
Integration.",
                        strings.Join(missingKamelets, ","))
                cond = corev1.ConditionUnknown
        }
@@ -189,12 +191,44 @@ func (t *kameletsTrait) collectKamelets(e *Environment) 
(map[string]*v1.Kamelet,
 
 // calculateNamespaces is in charge to scan the kamelets specification and 
provide a list of
 // namespaces where to look for Kamelets.
-func (t *kameletsTrait) calculateNamespaces(defaultNamespaces ...string) 
([]string, error) {
-       return calculateNamespaces(strings.Split(t.List, ","), 
defaultNamespaces...)
+func (t *kameletsTrait) calculateNamespaces(e *Environment, defaultNamespaces 
...string) ([]string, error) {
+       namespaces, err := calculateNamespaces(strings.Split(t.List, ","))
+       if err != nil {
+               return namespaces, err
+       }
+       if len(namespaces) > 0 {
+               if e.Integration.Spec.ServiceAccountName == "" {
+                       return nil, fmt.Errorf("you must to use an authorized 
ServiceAccount to access cross-namespace resources kamelets. " +
+                               "Set it in the Integration spec accordingly")
+               }
+               // verify an SA exists and it is authorized for Kamelets in 
that namespace
+               for _, ns := range namespaces {
+                       ok, err := kubernetes.CheckServiceAccountPermission(
+                               e.Ctx,
+                               e.Client,
+                               fmt.Sprintf("system:serviceaccount:%s:%s", 
e.Integration.Namespace, e.Integration.Spec.ServiceAccountName),
+                               v1.SchemeGroupVersion.Group,
+                               "kamelets",
+                               ns,
+                               "get",
+                       )
+                       if err != nil {
+                               return nil, err
+                       }
+                       if !ok {
+                               return nil, fmt.Errorf("cross-namespace 
Integration reference authorization denied for the ServiceAccount %s and 
resources kamelets",
+                                       e.Integration.Spec.ServiceAccountName)
+                       }
+               }
+       }
+
+       // Also append the default namespaces
+       namespaces = append(namespaces, defaultNamespaces...)
+       return namespaces, nil
 }
 
-func calculateNamespaces(kamelets []string, defaultNamespaces ...string) 
([]string, error) {
-       namespaces := defaultNamespaces
+func calculateNamespaces(kamelets []string) ([]string, error) {
+       var namespaces []string
        for _, kml := range kamelets {
                ns, err := getKameletNamespace(kml)
                if err != nil {
diff --git a/pkg/trait/kamelets_test.go b/pkg/trait/kamelets_test.go
index 48dc27fcb..9c0944b4c 100644
--- a/pkg/trait/kamelets_test.go
+++ b/pkg/trait/kamelets_test.go
@@ -773,14 +773,11 @@ func TestKameletNamespaceParameter(t *testing.T) {
 func TestCalculateKameletNamespaces(t *testing.T) {
        namespaces, err := calculateNamespaces(
                []string{"my-kamelet", "my-kamelet?kameletNamespace=ns1", 
"my-kamelet?kameletVersion=v2&kameletNamespace=ns2"},
-               "default", "camel-k",
        )
        require.NoError(t, err)
-       assert.Len(t, namespaces, 4)
+       assert.Len(t, namespaces, 2)
        assert.Contains(t, namespaces, "ns1")
        assert.Contains(t, namespaces, "ns2")
-       assert.Contains(t, namespaces, "default")
-       assert.Contains(t, namespaces, "camel-k")
 }
 
 func TestKameletMultiNamespace(t *testing.T) {
@@ -794,7 +791,7 @@ func TestKameletMultiNamespace(t *testing.T) {
                flow,
                &v1.Kamelet{
                        ObjectMeta: metav1.ObjectMeta{
-                               Namespace: "test",
+                               Namespace: "default",
                                Name:      "timer",
                        },
                        Spec: v1.KameletSpec{
@@ -828,6 +825,20 @@ func TestKameletMultiNamespace(t *testing.T) {
        assert.True(t, enabled)
        assert.Nil(t, condition)
 
+       // Must fail, no ServiceAccount
+       err = trait.Apply(environment)
+       require.Error(t, err)
+       assert.Equal(t, "you must to use an authorized ServiceAccount to access 
cross-namespace resources kamelets. "+
+               "Set it in the Integration spec accordingly", err.Error())
+       // Must fail, unauthorized ServiceAccount
+       environment.Integration.Spec.ServiceAccountName = "unauth-sa"
+       err = trait.Apply(environment)
+       require.Error(t, err)
+       assert.Equal(t, "cross-namespace Integration reference authorization 
denied for the ServiceAccount unauth-sa "+
+               "and resources kamelets", err.Error())
+       // Now we should good to go
+       environment.Integration.Namespace = "default"
+       environment.Integration.Spec.ServiceAccountName = "cross-ns-sa"
        err = trait.Apply(environment)
        require.NoError(t, err)
        assert.Equal(t, "extra?kameletNamespace=ns1,timer", trait.List)
@@ -850,7 +861,7 @@ func TestKameletMultiNamespaceMissing(t *testing.T) {
                flow,
                &v1.Kamelet{
                        ObjectMeta: metav1.ObjectMeta{
-                               Namespace: "test",
+                               Namespace: "default",
                                Name:      "timer",
                        },
                        Spec: v1.KameletSpec{
@@ -879,6 +890,9 @@ func TestKameletMultiNamespaceMissing(t *testing.T) {
                        },
                })
 
+       // Cross namespaces authorized user
+       environment.Integration.Namespace = "default"
+       environment.Integration.Spec.ServiceAccountName = "cross-ns-sa"
        enabled, condition, err := trait.Configure(environment)
        require.NoError(t, err)
        assert.True(t, enabled)
diff --git a/pkg/util/bindings/api.go b/pkg/util/bindings/api.go
index 47dbe7ff2..80f3d142a 100644
--- a/pkg/util/bindings/api.go
+++ b/pkg/util/bindings/api.go
@@ -57,11 +57,12 @@ type BindingProvider interface {
 
 //nolint:containedctx
 type BindingContext struct {
-       Ctx       context.Context
-       Client    client.Client
-       Namespace string
-       Profile   v1.TraitProfile
-       Metadata  map[string]string
+       Ctx                context.Context
+       Client             client.Client
+       Namespace          string
+       Profile            v1.TraitProfile
+       Metadata           map[string]string
+       ServiceAccountName string
 }
 
 type EndpointContext struct {
diff --git a/pkg/util/bindings/catalog.go b/pkg/util/bindings/catalog.go
index 3c0c7c8f6..37be81be2 100644
--- a/pkg/util/bindings/catalog.go
+++ b/pkg/util/bindings/catalog.go
@@ -21,8 +21,10 @@ import (
        "errors"
        "fmt"
        "sort"
+       "strings"
 
        v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+       "github.com/apache/camel-k/v2/pkg/util/kubernetes"
        "k8s.io/utils/ptr"
 )
 
@@ -78,6 +80,38 @@ func validateEndpoint(ctx BindingContext, e v1.Endpoint) 
error {
                        }
                        return errors.New("cross-namespace Pipe references are 
not allowed for Knative")
                }
+               // only check this when there is a cross-namespace access
+               return verifyResourceRBAC(ctx, e)
+       }
+
+       return nil
+}
+
+// verifyResourceRBAC verify if the ServiceAccount which is running the 
Integration has enough
+// privileges to access the resource. We avoid any potential cross-namespace 
vulnerability access.
+func verifyResourceRBAC(ctx BindingContext, e v1.Endpoint) error {
+       resources := strings.ToLower(e.Ref.Kind) + "s"
+       if ctx.ServiceAccountName == "" {
+               return fmt.Errorf("you must to use an authorized ServiceAccount 
to access cross-namespace resources %s. "+
+                       "Set it in the Pipe spec accordingly", resources)
+       }
+       ok, err := kubernetes.CheckServiceAccountPermission(
+               ctx.Ctx,
+               ctx.Client,
+               fmt.Sprintf("system:serviceaccount:%s:%s", ctx.Namespace, 
ctx.ServiceAccountName),
+               e.Ref.GroupVersionKind().Group,
+               resources,
+               e.Ref.Namespace,
+               "get",
+       )
+
+       if err != nil {
+               return err
+       }
+
+       if !ok {
+               return fmt.Errorf("cross-namespace Pipe reference authorization 
denied for the ServiceAccount %s and resources %s",
+                       ctx.ServiceAccountName, resources)
        }
        return nil
 }
diff --git a/pkg/util/bindings/catalog_test.go 
b/pkg/util/bindings/catalog_test.go
index 53538280d..5a73b68db 100644
--- a/pkg/util/bindings/catalog_test.go
+++ b/pkg/util/bindings/catalog_test.go
@@ -122,9 +122,6 @@ func TestValidateEndpoint(t *testing.T) {
 
        for i, tc := range testcases {
                t.Run(fmt.Sprintf("test-%d-%s", i, tc.name), func(t *testing.T) 
{
-                       if tc.operatorNamespace != "" {
-                               t.Setenv("NAMESPACE", tc.operatorNamespace)
-                       }
 
                        ctx, cancel := context.WithCancel(context.Background())
                        defer cancel()
@@ -138,6 +135,11 @@ func TestValidateEndpoint(t *testing.T) {
                                Namespace: tc.namespace,
                                Profile:   v1.DefaultTraitProfile,
                        }
+                       if tc.operatorNamespace != "" {
+                               // special privileges required for cross 
namespace
+                               bindingContext.ServiceAccountName = 
"cross-ns-sa"
+                               bindingContext.Namespace = "default"
+                       }
 
                        err = validateEndpoint(bindingContext, tc.endpoint)
                        require.NoError(t, err)
@@ -167,6 +169,33 @@ func TestValidateEndpointErrorRefURI(t *testing.T) {
 }
 
 func TestValidateEndpointKameletCrossNS(t *testing.T) {
+       client, err := internal.NewFakeClient()
+       require.NoError(t, err)
+
+       endpoint := v1.Endpoint{
+               Ref: &corev1.ObjectReference{
+                       Kind:       v1.KameletKind,
+                       APIVersion: v1.SchemeGroupVersion.String(),
+                       Name:       "foo-kamelet",
+                       Namespace:  "kamelet-ns",
+               },
+       }
+
+       bindingContext := BindingContext{
+               Namespace:          "default",
+               Client:             client,
+               Ctx:                context.Background(),
+               ServiceAccountName: "cross-ns-sa",
+       }
+
+       err = validateEndpoint(bindingContext, endpoint)
+       require.NoError(t, err)
+}
+
+func TestValidateEndpointKameletCrossNSNoSA(t *testing.T) {
+       client, err := internal.NewFakeClient()
+       require.NoError(t, err)
+
        endpoint := v1.Endpoint{
                Ref: &corev1.ObjectReference{
                        Kind:       v1.KameletKind,
@@ -178,10 +207,40 @@ func TestValidateEndpointKameletCrossNS(t *testing.T) {
 
        bindingContext := BindingContext{
                Namespace: "default",
+               Client:    client,
+               Ctx:       context.Background(),
        }
 
-       err := validateEndpoint(bindingContext, endpoint)
+       err = validateEndpoint(bindingContext, endpoint)
+       require.Error(t, err)
+       require.Equal(t, "you must to use an authorized ServiceAccount to 
access cross-namespace resources kamelets. "+
+               "Set it in the Pipe spec accordingly", err.Error())
+}
+
+func TestValidateEndpointKameletCrossNSDenied(t *testing.T) {
+       client, err := internal.NewFakeClient()
        require.NoError(t, err)
+
+       endpoint := v1.Endpoint{
+               Ref: &corev1.ObjectReference{
+                       Kind:       v1.KameletKind,
+                       APIVersion: v1.SchemeGroupVersion.String(),
+                       Name:       "foo-kamelet",
+                       Namespace:  "kamelet-ns",
+               },
+       }
+
+       bindingContext := BindingContext{
+               Namespace:          "default",
+               Client:             client,
+               Ctx:                context.Background(),
+               ServiceAccountName: "my-sa",
+       }
+
+       err = validateEndpoint(bindingContext, endpoint)
+       require.Error(t, err)
+       require.Equal(t, "cross-namespace Pipe reference authorization denied 
for the ServiceAccount my-sa"+
+               " and resources kamelets", err.Error())
 }
 
 func TestValidateEndpointErrorKnativeCrossNS(t *testing.T) {
diff --git a/pkg/util/kubernetes/permission.go 
b/pkg/util/kubernetes/permission.go
index e8f3c9466..1f3af7e1c 100644
--- a/pkg/util/kubernetes/permission.go
+++ b/pkg/util/kubernetes/permission.go
@@ -26,10 +26,10 @@ import (
        "k8s.io/client-go/kubernetes"
 )
 
-// CheckPermission can be used to check if the current user/service-account is 
allowed to execute a given operation
+// CheckSelfPermission can be used to check if the current 
user/service-account is allowed to execute a given operation
 // in the cluster.
 // E.g. checkPermission(client, olmv1alpha1.GroupName, 
"clusterserviceversions", namespace, "camel-k", "get").
-func CheckPermission(ctx context.Context, client kubernetes.Interface, group, 
resource, namespace, name, verb string) (bool, error) {
+func CheckSelfPermission(ctx context.Context, client kubernetes.Interface, 
group, resource, namespace, name, verb string) (bool, error) {
        sarReview := &authorizationv1.SelfSubjectAccessReview{
                Spec: authorizationv1.SelfSubjectAccessReviewSpec{
                        ResourceAttributes: &authorizationv1.ResourceAttributes{
@@ -52,3 +52,29 @@ func CheckPermission(ctx context.Context, client 
kubernetes.Interface, group, re
 
        return sar.Status.Allowed, nil
 }
+
+// CheckServiceAccountPermission verify if a given Service Account can access 
a given resource.
+// Service Account must be provided as "system:serviceaccount:namespace:name" 
format.
+func CheckServiceAccountPermission(ctx context.Context, client 
kubernetes.Interface, sa, group, resources, namespace, verb string) (bool, 
error) {
+       sarReview := &authorizationv1.SubjectAccessReview{
+               Spec: authorizationv1.SubjectAccessReviewSpec{
+                       User: sa,
+                       ResourceAttributes: &authorizationv1.ResourceAttributes{
+                               Group:     group,
+                               Namespace: namespace,
+                               Resource:  resources,
+                               Verb:      verb,
+                       },
+               },
+       }
+
+       sar, err := client.AuthorizationV1().SubjectAccessReviews().Create(ctx, 
sarReview, metav1.CreateOptions{})
+       if err != nil {
+               if k8serrors.IsForbidden(err) {
+                       return false, nil
+               }
+               return false, err
+       }
+
+       return sar.Status.Allowed, nil
+}


Reply via email to