You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by pc...@apache.org on 2026/09/23 05:56:53 UTC

(camel-k) 01/02: Fix #6778: Validate service account permission by resource name

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

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

commit 3e7578b08666619a33a9db473321decc5def1761
Author: AllanAlmeida <al...@gmail.com>
AuthorDate: Thu Sep 17 11:51:44 2026 -0300

    Fix #6778: Validate service account permission by resource name
    
    Generated-by: OpenAI Codex
---
 pkg/internal/client.go                 |  3 +-
 pkg/trait/kamelets.go                  | 53 +++++++++++++++-------------
 pkg/trait/kamelets_test.go             | 23 +++++++++++++
 pkg/util/bindings/catalog.go           |  1 +
 pkg/util/bindings/catalog_test.go      | 26 ++++++++++++++
 pkg/util/kubernetes/permission.go      |  5 +--
 pkg/util/kubernetes/permission_test.go | 63 ++++++++++++++++++++++++++++++++++
 7 files changed, 148 insertions(+), 26 deletions(-)

diff --git a/pkg/internal/client.go b/pkg/internal/client.go
index 50f014123..3de56810c 100644
--- a/pkg/internal/client.go
+++ b/pkg/internal/client.go
@@ -269,7 +269,8 @@ type FakeSAR struct {
 // Create 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"
+	allowed := sar.Spec.User == "system:serviceaccount:default:cross-ns-sa" && ra.Verb == "get" &&
+		ra.Resource == "kamelets" && ra.Name != "restricted-kamelet"
 
 	sar.Status.Allowed = allowed
 	sar.Status.Reason = "mocked"
diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go
index a2b421f96..6b3c16133 100644
--- a/pkg/trait/kamelets.go
+++ b/pkg/trait/kamelets.go
@@ -202,33 +202,40 @@ 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(e *Environment, defaultNamespaces ...string) ([]string, error) {
-	namespaces, err := calculateNamespaces(strings.Split(t.List, ","))
+	kamelets := strings.Split(t.List, ",")
+	namespaces, err := calculateNamespaces(kamelets)
 	if err != nil {
 		return namespaces, err
 	}
-	if len(namespaces) > 0 {
-		if e.Integration.Spec.ServiceAccountName == "" {
-			return nil, errors.New("you must to use an authorized ServiceAccount to access cross-namespace resources kamelets. " +
-				"Set it in the Integration spec accordingly")
+	if len(namespaces) > 0 && e.Integration.Spec.ServiceAccountName == "" {
+		return nil, errors.New("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 each Kamelet
+	for _, kml := range kamelets {
+		ns, err := getKameletNamespace(kml)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse kamelet namespace: %w", err)
 		}
-		// 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)
-			}
+		if ns == "" {
+			continue
+		}
+		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,
+			getKameletKey(kml),
+			"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)
 		}
 	}
 
diff --git a/pkg/trait/kamelets_test.go b/pkg/trait/kamelets_test.go
index 3afcaf77a..91652f4dd 100644
--- a/pkg/trait/kamelets_test.go
+++ b/pkg/trait/kamelets_test.go
@@ -850,6 +850,29 @@ func TestKameletMultiNamespace(t *testing.T) {
 		"Kamelets [extra,timer] found in cluster")
 }
 
+func TestKameletMultiNamespaceDeniedResource(t *testing.T) {
+	flow := `
+- from:
+    uri: kamelet:timer
+    steps:
+    - to: kamelet:extra?kameletNamespace=ns1
+    - to: kamelet:restricted-kamelet?kameletNamespace=ns1
+`
+	trait, environment := createKameletsTestEnvironment(flow)
+	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)
+	assert.Nil(t, condition)
+
+	err = trait.Apply(environment)
+	require.Error(t, err)
+	assert.Equal(t, "cross-namespace Integration reference authorization denied for the ServiceAccount cross-ns-sa "+
+		"and resources kamelets", err.Error())
+}
+
 func TestKameletMultiNamespaceMissing(t *testing.T) {
 	flow := `
 - from:
diff --git a/pkg/util/bindings/catalog.go b/pkg/util/bindings/catalog.go
index 6e9f5bd33..36e6eb88b 100644
--- a/pkg/util/bindings/catalog.go
+++ b/pkg/util/bindings/catalog.go
@@ -105,6 +105,7 @@ func verifyResourceRBAC(ctx BindingContext, e v1.Endpoint) error {
 		e.Ref.GroupVersionKind().Group,
 		resources,
 		e.Ref.Namespace,
+		e.Ref.Name,
 		"get",
 	)
 
diff --git a/pkg/util/bindings/catalog_test.go b/pkg/util/bindings/catalog_test.go
index 5a73b68db..44e0f2422 100644
--- a/pkg/util/bindings/catalog_test.go
+++ b/pkg/util/bindings/catalog_test.go
@@ -192,6 +192,32 @@ func TestValidateEndpointKameletCrossNS(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func TestValidateEndpointKameletCrossNSDeniedResource(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:       "restricted-kamelet",
+			Namespace:  "kamelet-ns",
+		},
+	}
+
+	bindingContext := BindingContext{
+		Namespace:          "default",
+		Client:             client,
+		Ctx:                context.Background(),
+		ServiceAccountName: "cross-ns-sa",
+	}
+
+	err = validateEndpoint(bindingContext, endpoint)
+	require.Error(t, err)
+	require.Equal(t, "cross-namespace Pipe reference authorization denied for the ServiceAccount cross-ns-sa"+
+		" and resources kamelets", err.Error())
+}
+
 func TestValidateEndpointKameletCrossNSNoSA(t *testing.T) {
 	client, err := internal.NewFakeClient()
 	require.NoError(t, err)
diff --git a/pkg/util/kubernetes/permission.go b/pkg/util/kubernetes/permission.go
index 867d93019..7e2870aec 100644
--- a/pkg/util/kubernetes/permission.go
+++ b/pkg/util/kubernetes/permission.go
@@ -56,14 +56,15 @@ func CheckSelfPermission(ctx context.Context, client kubernetes.Interface, group
 
 // 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) {
+func CheckServiceAccountPermission(ctx context.Context, client kubernetes.Interface, sa, group, resource, namespace, name, verb string) (bool, error) {
 	sarReview := &authorizationv1.SubjectAccessReview{
 		Spec: authorizationv1.SubjectAccessReviewSpec{
 			User: sa,
 			ResourceAttributes: &authorizationv1.ResourceAttributes{
 				Group:     group,
 				Namespace: namespace,
-				Resource:  resources,
+				Resource:  resource,
+				Name:      name,
 				Verb:      verb,
 			},
 		},
diff --git a/pkg/util/kubernetes/permission_test.go b/pkg/util/kubernetes/permission_test.go
new file mode 100644
index 000000000..10ed4f878
--- /dev/null
+++ b/pkg/util/kubernetes/permission_test.go
@@ -0,0 +1,63 @@
+/*
+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 kubernetes
+
+import (
+	"context"
+	"testing"
+
+	authorizationv1 "k8s.io/api/authorization/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/kubernetes/fake"
+	k8stesting "k8s.io/client-go/testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestCheckServiceAccountPermissionByResourceName(t *testing.T) {
+	client := fake.NewSimpleClientset()
+	client.PrependReactor("create", "subjectaccessreviews", func(action k8stesting.Action) (bool, runtime.Object, error) {
+		createAction, ok := action.(k8stesting.CreateAction)
+		require.True(t, ok)
+		review, ok := createAction.GetObject().(*authorizationv1.SubjectAccessReview)
+		require.True(t, ok)
+		require.Equal(t, "system:serviceaccount:source:integration", review.Spec.User)
+		require.Equal(t, "camel.apache.org", review.Spec.ResourceAttributes.Group)
+		require.Equal(t, "kamelets", review.Spec.ResourceAttributes.Resource)
+		require.Equal(t, "target", review.Spec.ResourceAttributes.Namespace)
+		require.Equal(t, "get", review.Spec.ResourceAttributes.Verb)
+
+		review.Status.Allowed = review.Spec.ResourceAttributes.Name == "allowed-kamelet"
+
+		return true, review, nil
+	})
+
+	allowed, err := CheckServiceAccountPermission(
+		context.Background(), client, "system:serviceaccount:source:integration",
+		"camel.apache.org", "kamelets", "target", "allowed-kamelet", "get",
+	)
+	require.NoError(t, err)
+	require.True(t, allowed)
+
+	allowed, err = CheckServiceAccountPermission(
+		context.Background(), client, "system:serviceaccount:source:integration",
+		"camel.apache.org", "kamelets", "target", "denied-kamelet", "get",
+	)
+	require.NoError(t, err)
+	require.False(t, allowed)
+}